<!-- build time:Tue Feb 18 2020 17:19:28 GMT+0800 (China Standard Time) --><!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui"><meta name="renderer" content="webkit"><meta http-equiv="Cache-Control" content="no-transform"><meta http-equiv="Cache-Control" content="no-siteapp"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><meta name="format-detection" content="telephone=no,email=no,adress=no"><meta name="theme-color" content="#000000"><meta http-equiv="window-target" content="_top"><title>Spring Boot SpringMVC 入门 | calm&#39;s blog</title><meta name="description" content="摘要: 原创出处 http:&#x2F;&#x2F;www.iocoder.cn&#x2F;Spring-Boot&#x2F;SpringMVC&#x2F; 「芋道源码」，卡姆在此基础上略作修改，欢迎转载，保留摘要，谢谢！1. 概述关于这个问题，如果感兴趣，可以看看 《Struts2 和 SpringMVC 区别？》 讨论。而现在，SpringMVC 基本已经统治 Web MVC 框架，相信胖友已经很少接触非使用 SpringMVC 的项目了。在"><meta property="og:type" content="article"><meta property="og:title" content="Spring Boot SpringMVC 入门"><meta property="og:url" content="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/index.html"><meta property="og:site_name" content="卡姆"><meta property="og:description" content="摘要: 原创出处 http:&#x2F;&#x2F;www.iocoder.cn&#x2F;Spring-Boot&#x2F;SpringMVC&#x2F; 「芋道源码」，卡姆在此基础上略作修改，欢迎转载，保留摘要，谢谢！1. 概述关于这个问题，如果感兴趣，可以看看 《Struts2 和 SpringMVC 区别？》 讨论。而现在，SpringMVC 基本已经统治 Web MVC 框架，相信胖友已经很少接触非使用 SpringMVC 的项目了。在"><meta property="og:locale" content="zh_CN"><meta property="og:image" content="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/1.png"><meta property="og:image" content="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/2.png"><meta property="og:image" content="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/3.png"><meta property="og:image" content="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/04.png"><meta property="og:image" content="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/05.png"><meta property="og:image" content="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/06.png"><meta property="article:published_time" content="2019-12-29T14:37:47.000Z"><meta property="article:modified_time" content="2020-01-07T06:20:58.417Z"><meta property="article:author" content="calm"><meta property="article:tag" content="SpringMVC"><meta name="twitter:card" content="summary"><meta name="twitter:image" content="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/1.png"><link rel="canonical" href="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/index.html"><link rel="alternate" href="/atom.xml" title="卡姆" type="application/atom+xml"><link rel="icon" href="/blog/images/avatar.jpg" type="image/x-icon"><link rel="stylesheet" href="/blog/css/style.css"><link rel="stylesheet" href="//cdn.jsdelivr.net/npm/gitalk@1.4.0/dist/gitalk.min.css"><meta name="generator" content="Hexo 4.2.0"></head><body class="main-center theme-green" itemscope itemtype="http://schema.org/WebPage"><header class="header" itemscope itemtype="http://schema.org/WPHeader"><div class="slimContent"><div class="navbar-header"><div class="profile-block text-center"><a id="avatar" href="https://github.com/yuyong725" target="_blank"><img class="img-circle img-rotate" src="/blog/images/avatar.jpg" width="200" height="200"></a><h2 id="name" class="hidden-xs hidden-sm">卡姆</h2><h3 id="title" class="hidden-xs hidden-sm hidden-md">a Java coder need calm</h3><small id="location" class="text-muted hidden-xs hidden-sm"><i class="icon icon-map-marker"></i> hangzhou, China</small></div><div class="search" id="search-form-wrap"><form class="search-form sidebar-form"><div class="input-group"><input type="text" class="search-form-input form-control" placeholder="搜索"> <span class="input-group-btn"><button type="submit" class="search-form-submit btn btn-flat" onclick="return!1"><i class="icon icon-search"></i></button></span></div></form><div class="ins-search"><div class="ins-search-mask"></div><div class="ins-search-container"><div class="ins-input-wrapper"><input type="text" class="ins-search-input" placeholder="想要查找什么..." x-webkit-speech> <button type="button" class="close ins-close ins-selectable" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button></div><div class="ins-section-wrapper"><div class="ins-section-container"></div></div></div></div></div><button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target="#main-navbar" aria-controls="main-navbar" aria-expanded="false"><span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span></button></div><nav id="main-navbar" class="collapse navbar-collapse" itemscope itemtype="http://schema.org/SiteNavigationElement" role="navigation"><ul class="nav navbar-nav main-nav"><li class="menu-item menu-item-home"><a href="/blog/."><i class="icon icon-home-fill"></i> <span class="menu-title">首页</span></a></li><li class="menu-item menu-item-archives"><a href="/blog/archives"><i class="icon icon-archives-fill"></i> <span class="menu-title">归档</span></a></li><li class="menu-item menu-item-categories"><a href="/blog/categories"><i class="icon icon-folder"></i> <span class="menu-title">分类</span></a></li><li class="menu-item menu-item-tags"><a href="/blog/tags"><i class="icon icon-tags"></i> <span class="menu-title">标签</span></a></li><li class="menu-item menu-item-repository"><a href="/blog/repository"><i class="icon icon-project"></i> <span class="menu-title">项目</span></a></li><li class="menu-item menu-item-books"><a href="/blog/books"><i class="icon icon-book-fill"></i> <span class="menu-title">书单</span></a></li><li class="menu-item menu-item-links"><a href="/blog/links"><i class="icon icon-friendship"></i> <span class="menu-title">友链</span></a></li><li class="menu-item menu-item-about"><a href="/blog/about"><i class="icon icon-cup-fill"></i> <span class="menu-title">关于</span></a></li></ul><ul class="social-links"><li><a href="https://github.com/yuyong725" target="_blank" title="Github" data-toggle="tooltip" data-placement="top"><i class="icon icon-github"></i></a></li></ul></nav></div></header><aside class="sidebar" itemscope itemtype="http://schema.org/WPSideBar"><div class="slimContent"><div class="widget"><h3 class="widget-title">公告</h3><div class="widget-body"><div id="board"><div class="content"><p>欢迎交流与分享经验!</p></div></div></div></div><div class="widget"><h3 class="widget-title">分类</h3><div class="widget-body"><ul class="category-list"><li class="category-list-item"><a class="category-list-link" href="/blog/categories/CI-DI/">CI/DI</a><span class="category-list-count">1</span></li><li class="category-list-item"><a class="category-list-link" href="/blog/categories/git/">git</a><span class="category-list-count">1</span></li><li class="category-list-item"><a class="category-list-link" href="/blog/categories/labs/">labs</a><span class="category-list-count">14</span></li></ul></div></div><div class="widget"><h3 class="widget-title">标签</h3><div class="widget-body"><ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/MyBatis/" rel="tag">MyBatis</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/Oauth2/" rel="tag">Oauth2</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/Spring-Security/" rel="tag">Spring Security</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/SpringMVC/" rel="tag">SpringMVC</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/docker/" rel="tag">docker</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/dubbo/" rel="tag">dubbo</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/git/" rel="tag">git</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/jdbctemplate/" rel="tag">jdbctemplate</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/jerkins/" rel="tag">jerkins</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/job/" rel="tag">job</a><span class="tag-list-count">2</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/jpa/" rel="tag">jpa</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/pdf/" rel="tag">pdf</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/redis/" rel="tag">redis</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/" rel="tag">分库分表</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90/" rel="tag">多数据源</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/" rel="tag">数据库版本管理</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5%E6%B1%A0/" rel="tag">数据库连接池</a><span class="tag-list-count">1</span></li><li class="tag-list-item"><a class="tag-list-link" href="/blog/tags/%E7%BC%93%E5%AD%98/" rel="tag">缓存</a><span class="tag-list-count">2</span></li></ul></div></div><div class="widget"><h3 class="widget-title">标签云</h3><div class="widget-body tagcloud"><a href="/blog/tags/MyBatis/" style="font-size:13px">MyBatis</a> <a href="/blog/tags/Oauth2/" style="font-size:13px">Oauth2</a> <a href="/blog/tags/Spring-Security/" style="font-size:13px">Spring Security</a> <a href="/blog/tags/SpringMVC/" style="font-size:13px">SpringMVC</a> <a href="/blog/tags/docker/" style="font-size:13px">docker</a> <a href="/blog/tags/dubbo/" style="font-size:13px">dubbo</a> <a href="/blog/tags/git/" style="font-size:13px">git</a> <a href="/blog/tags/jdbctemplate/" style="font-size:13px">jdbctemplate</a> <a href="/blog/tags/jerkins/" style="font-size:13px">jerkins</a> <a href="/blog/tags/job/" style="font-size:14px">job</a> <a href="/blog/tags/jpa/" style="font-size:13px">jpa</a> <a href="/blog/tags/pdf/" style="font-size:13px">pdf</a> <a href="/blog/tags/redis/" style="font-size:13px">redis</a> <a href="/blog/tags/%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/" style="font-size:13px">分库分表</a> <a href="/blog/tags/%E5%A4%9A%E6%95%B0%E6%8D%AE%E6%BA%90/" style="font-size:13px">多数据源</a> <a href="/blog/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86/" style="font-size:13px">数据库版本管理</a> <a href="/blog/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5%E6%B1%A0/" style="font-size:13px">数据库连接池</a> <a href="/blog/tags/%E7%BC%93%E5%AD%98/" style="font-size:14px">缓存</a></div></div><div class="widget"><h3 class="widget-title">归档</h3><div class="widget-body"><ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/blog/archives/2020/02/">二月 2020</a><span class="archive-list-count">5</span></li><li class="archive-list-item"><a class="archive-list-link" href="/blog/archives/2020/01/">一月 2020</a><span class="archive-list-count">8</span></li><li class="archive-list-item"><a class="archive-list-link" href="/blog/archives/2019/12/">十二月 2019</a><span class="archive-list-count">4</span></li></ul></div></div><div class="widget"><h3 class="widget-title">最新文章</h3><div class="widget-body"><ul class="recent-post-list list-unstyled no-thumbnail"><li><div class="item-inner"><p class="item-category"><a class="category-link" href="/blog/categories/labs/">labs</a></p><p class="item-title"><a href="/blog/2020/02/18/labs-springboot2-async-Job/" class="title">Spring Boot 异步任务入门</a></p><p class="item-date"><time datetime="2020-02-18T05:10:39.000Z" itemprop="datePublished">2020-02-18</time></p></div></li><li><div class="item-inner"><p class="item-category"><a class="category-link" href="/blog/categories/labs/">labs</a></p><p class="item-title"><a href="/blog/2020/02/18/labs-springboot2-job/" class="title">Spring Boot 定时任务入门</a></p><p class="item-date"><time datetime="2020-02-18T02:26:31.000Z" itemprop="datePublished">2020-02-18</time></p></div></li><li><div class="item-inner"><p class="item-category"><a class="category-link" href="/blog/categories/labs/">labs</a></p><p class="item-title"><a href="/blog/2020/02/17/labs-springboot2-database-version-control/" class="title">Spring Boot 数据库版本管理入门</a></p><p class="item-date"><time datetime="2020-02-17T12:05:54.000Z" itemprop="datePublished">2020-02-17</time></p></div></li><li><div class="item-inner"><p class="item-category"><a class="category-link" href="/blog/categories/labs/">labs</a></p><p class="item-title"><a href="/blog/2020/02/17/labs-springboot2-sharding-datasource/" class="title">Spring Boot 分库分表入门</a></p><p class="item-date"><time datetime="2020-02-17T08:15:31.000Z" itemprop="datePublished">2020-02-17</time></p></div></li><li><div class="item-inner"><p class="item-category"><a class="category-link" href="/blog/categories/labs/">labs</a></p><p class="item-title"><a href="/blog/2020/02/13/labs-springboot2-dynamic-datasource/" class="title">Spring Boot 多数据源（读写分离）入门</a></p><p class="item-date"><time datetime="2020-02-13T03:18:37.000Z" itemprop="datePublished">2020-02-13</time></p></div></li></ul></div></div></div></aside><aside class="sidebar sidebar-toc collapse in" id="collapseToc" itemscope itemtype="http://schema.org/WPSideBar"><div class="slimContent"><nav id="toc" class="article-toc"><h3 class="toc-title">文章目录</h3><ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#1-概述"><span class="toc-text">1. 概述</span></a></li><li class="toc-item toc-level-1"><a class="toc-link" href="#2-快速入门"><span class="toc-text">2. 快速入门</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#2-1-注解"><span class="toc-text">2.1 注解</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#2-1-1-Controller"><span class="toc-text">2.1.1 @Controller</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2-1-2-RequestMapping"><span class="toc-text">2.1.2 @RequestMapping</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2-1-3-RequestParam"><span class="toc-text">2.1.3 @RequestParam</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-2-引入依赖"><span class="toc-text">2.2 引入依赖</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-3-Application"><span class="toc-text">2.3 Application</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-4-UserController"><span class="toc-text">2.4 UserController</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#2-5-UserController2"><span class="toc-text">2.5 UserController2</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#3-测试接口"><span class="toc-text">3. 测试接口</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#3-1-集成测试"><span class="toc-text">3.1 集成测试</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#3-2-单元测试"><span class="toc-text">3.2 单元测试</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#4-全局统一返回"><span class="toc-text">4. 全局统一返回</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#4-1-引入依赖"><span class="toc-text">4.1 引入依赖</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-2-Application"><span class="toc-text">4.2 Application</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-3-CommonResult"><span class="toc-text">4.3 CommonResult</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-4-GlobalResponseBodyHandler"><span class="toc-text">4.4 GlobalResponseBodyHandler</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-5-UserController"><span class="toc-text">4.5 UserController</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#4-6-小小的讨论"><span class="toc-text">4.6 小小的讨论</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#5-全局异常处理"><span class="toc-text">5. 全局异常处理</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#5-1-ServiceExceptionEnum"><span class="toc-text">5.1 ServiceExceptionEnum</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#5-2-ServiceException"><span class="toc-text">5.2 ServiceException</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#5-3-GlobalExceptionHandler"><span class="toc-text">5.3 GlobalExceptionHandler</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#5-4-UserController"><span class="toc-text">5.4 UserController</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#6-HandlerInterceptor-拦截器"><span class="toc-text">6. HandlerInterceptor 拦截器</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#6-1-自定义-HandlerInterceptor"><span class="toc-text">6.1 自定义 HandlerInterceptor</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#6-2-SpringMVCConfiguration"><span class="toc-text">6.2 SpringMVCConfiguration</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#6-3-UserController"><span class="toc-text">6.3 UserController</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#6-4-拓展阅读"><span class="toc-text">6.4 拓展阅读</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#7-Servlet、Filter、Listener"><span class="toc-text">7. Servlet、Filter、Listener</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#7-1-通过-Bean-的方式"><span class="toc-text">7.1 通过 Bean 的方式</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#7-2-通过注解的方式"><span class="toc-text">7.2 通过注解的方式</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#8-Cors-跨域"><span class="toc-text">8. Cors 跨域</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#8-1-CrossCors"><span class="toc-text">8.1 @CrossCors</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#8-2-CorsRegistry"><span class="toc-text">8.2 CorsRegistry</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#8-3-CorsFilter"><span class="toc-text">8.3 CorsFilter</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#9-HttpMessageConverter-消息转换器"><span class="toc-text">9. HttpMessageConverter 消息转换器</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#9-1-引入依赖"><span class="toc-text">9.1 引入依赖</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#9-2-SpringMVCConfiguration"><span class="toc-text">9.2 SpringMVCConfiguration</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#9-3-UserController"><span class="toc-text">9.3 UserController</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#10-整合-Fastjson"><span class="toc-text">10. 整合 Fastjson</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#10-1-引入依赖"><span class="toc-text">10.1 引入依赖</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#10-2-SpringMVCConfiguration"><span class="toc-text">10.2 SpringMVCConfiguration</span></a></li></ol></li><li class="toc-item toc-level-1"><a class="toc-link" href="#666-彩蛋"><span class="toc-text">666. 彩蛋</span></a></li></ol></nav></div></aside><main class="main" role="main"><div class="content"><article id="post-labs-springboot2-SpringMVC" class="article article-type-post" itemscope itemtype="http://schema.org/BlogPosting"><div class="article-header"><h1 class="article-title" itemprop="name">Spring Boot SpringMVC 入门</h1><div class="article-meta"><span class="article-date"><i class="icon icon-calendar-check"></i> <a href="/blog/2019/12/29/labs-springboot2-SpringMVC/" class="article-date"><time datetime="2019-12-29T14:37:47.000Z" itemprop="datePublished">2019-12-29</time></a></span> <span class="article-category"><i class="icon icon-folder"></i> <a class="article-category-link" href="/blog/categories/labs/">labs</a></span> <span class="article-tag"><i class="icon icon-tags"></i> <a class="article-tag-link" href="/blog/tags/SpringMVC/" rel="tag">SpringMVC</a></span> <span class="article-read hidden-xs"><i class="icon icon-eye-fill" aria-hidden="true"></i> <span id="busuanzi_container_page_pv"><span id="busuanzi_value_page_pv">0</span></span></span> <span class="post-comment"><i class="icon icon-comment"></i> <a href="/blog/2019/12/29/labs-springboot2-SpringMVC/#comments" class="article-comment-link">评论</a></span> <span class="post-wordcount hidden-xs" itemprop="wordCount">字数统计: 14.5k(字)</span> <span class="post-readcount hidden-xs" itemprop="timeRequired">阅读时长: 62(分)</span></div></div><div class="article-entry marked-body" itemprop="articleBody"><blockquote><p>摘要: 原创出处 <a href="http://www.iocoder.cn/Spring-Boot/SpringMVC/" target="_blank" rel="noopener">http://www.iocoder.cn/Spring-Boot/SpringMVC/</a> 「芋道源码」，卡姆在此基础上略作修改，欢迎转载，保留摘要，谢谢！</p></blockquote><h1 id="1-概述"><a href="#1-概述" class="headerlink" title="1. 概述"></a>1. 概述</h1><blockquote><p>关于这个问题，如果感兴趣，可以看看 <a href="https://www.zhihu.com/question/31565442" target="_blank" rel="noopener">《Struts2 和 SpringMVC 区别？》</a> 讨论。</p></blockquote><p>而现在，SpringMVC 基本已经统治 Web MVC 框架，相信胖友已经很少接触非使用 SpringMVC 的项目了。在艿艿实习那会，大概是 2011 年的时候，还经历了一次将项目从 Struts2 迁移到 SpringMVC 。</p><p>相比来说，SpringMVC 的易用性与性能都优于 Struts2 ，整体实现也更加清晰明了。当然，更更更重要的是，它有个好爸爸，Spring 极强的体系与社区活跃度。</p><p>因为是一篇 Spring Boot 集成 SpringMVC 入门的文章，卡姆就不多哔哔了，直接快速入门，遨游起来。不过还是提一句，SpringMVC 处理请求的整体流程，一定要能倒背如流。</p><h1 id="2-快速入门"><a href="#2-快速入门" class="headerlink" title="2. 快速入门"></a>2. 快速入门</h1><blockquote><p>示例代码对应仓库： <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-simple" target="_blank" rel="noopener">labs-springboot2-springmvc-simple</a> 。</p></blockquote><p>本小节，我们会使用 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web" target="_blank" rel="noopener">spring-boot-starter-web</a> 实现 SpringMVC 的自动化配置。然后实现用户的增删改查接口。接口列表如下：</p><table><thead><tr><th align="left">请求方法</th><th align="left">URL</th><th align="left">功能</th></tr></thead><tbody><tr><td align="left"><code>GET</code></td><td align="left"><code>/users</code></td><td align="left">查询用户列表</td></tr><tr><td align="left"><code>GET</code></td><td align="left"><code>/users/{id}</code></td><td align="left">获得指定用户编号的用户</td></tr><tr><td align="left"><code>POST</code></td><td align="left"><code>/users</code></td><td align="left">添加用户</td></tr><tr><td align="left"><code>PUT</code></td><td align="left"><code>/users/{id}</code></td><td align="left">更新指定用户编号的用户</td></tr><tr><td align="left"><code>DELETE</code></td><td align="left"><code>/users/{id}</code></td><td align="left">删除指定用户编号的用户</td></tr></tbody></table><p>下面，开始遨游~</p><h2 id="2-1-注解"><a href="#2-1-注解" class="headerlink" title="2.1 注解"></a>2.1 注解</h2><p>可能有胖友之前未使用过 SpringMVC ，所以在这个小节，我们来说下它提供的注解。</p><ul><li><code>@Controller</code></li><li><code>@RestController</code></li><li><code>@RequestMapping</code></li><li><code>@GetMapping</code></li><li><code>@PostMapping</code></li><li><code>@PutMapping</code></li><li><code>@RequestParam</code></li><li><code>@PathVariable</code></li></ul><p>已经了解过的胖友，可以快速略过或不看。</p><h3 id="2-1-1-Controller"><a href="#2-1-1-Controller" class="headerlink" title="2.1.1 @Controller"></a>2.1.1 @Controller</h3><p><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/stereotype/Controller.java" target="_blank" rel="noopener"><code>@Controller</code></a> 注解，添加在类上，表示这是控制器 Controller 对象。属性如下：</p><ul><li><code>name</code> 属性：该 Controller 对象的 Bean 名字。允许空。</li></ul><p><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/RestController.java" target="_blank" rel="noopener"><code>@RestController</code></a> 注解，添加在类上，是 <code>@Controller</code> 和 <a href="https://github.com/ndimiduk/spring-framework/blob/master/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/ResponseBody.java" target="_blank" rel="noopener"><code>@ResponseBody</code></a> 的组合注解，直接使用接口方法的返回结果，经过 JSON/XML 等序列化方式，最终返回。也就是说，无需使用 InternalResourceViewResolver 解析视图，返回 HTML 结果。</p><p>目前主流的架构，都是 <a href="https://blog.csdn.net/fuzhongmin05/article/details/81591072" target="_blank" rel="noopener">前后端分离</a> 的架构，后端只需要提供 API 接口，仅仅返回数据。而视图部分的工作，全部交给前端来做。也因此，我们项目中 99.99% 使用 <code>@RestController</code> 注解。</p><p>往往，我们提供的 API 接口，都是 Restful 或者类 Restful 风格，所以不了解的胖友，推荐看看如下两篇文章：</p><ul><li><a href="http://www.iocoder.cn/Fight/estful-api-best-practices.html" target="_blank" rel="noopener">《RESTful API 最佳实践》</a></li><li><a href="http://www.iocoder.cn/Fight/Learn-Restful-HTTP-API-design-from-Github" target="_blank" rel="noopener">《跟着 Github 学习 Restful HTTP API 的优雅设计》</a></li></ul><h3 id="2-1-2-RequestMapping"><a href="#2-1-2-RequestMapping" class="headerlink" title="2.1.2 @RequestMapping"></a>2.1.2 @RequestMapping</h3><p><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java" target="_blank" rel="noopener"><code>@RequestMapping</code></a> 注解，添加在类或方法上，标记该类/方法对应接口的配置信息。</p><p><code>@RequestMapping</code> 注解的<strong>常用属性</strong>，如下：</p><ul><li><code>path</code> 属性：接口路径。<code>[]</code> 数组，可以填写多个接口路径。</li><li><code>values</code> 属性：和 <code>path</code> 属性相同，是它的别名。</li><li><code>method</code> 属性：请求方法 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java" target="_blank" rel="noopener">RequestMethod</a> ，可以填写 <code>GET</code>、<code>POST</code>、<code>POST</code>、<code>DELETE</code> 等等。<code>[]</code> 数组，可以填写多个请求方法。如果为空，表示匹配所有请求方法。</li></ul><p><code>@RequestMapping</code> 注解的<strong>不常用属性</strong>，如下：</p><ul><li><p><code>name</code> 属性：接口名。一般情况下，我们不填写。</p></li><li><p><code>params</code> 属性：请求参数需要包含值的<strong>参数名</strong>。可以填写多个参数名。如果为空，表示匹配所有请你求方法。</p></li><li><p><code>headers</code> 属性：和 <code>params</code> 类似，只是从参数名变成<strong>请求头</strong>。</p></li><li><p><code>consumes</code> 属性：和 <code>params</code> 类似，只是从参数名变成请求头的<strong>提交内容类型</strong>( <a href="https://juejin.im/post/5cb34fc06fb9a068a75d3555" target="_blank" rel="noopener">Content-Type</a> )</p></li><li><blockquote><p>关于 <code>consumes</code> 和 <code>produces</code> 属性，可以看看 <a href="https://www.cnblogs.com/111testing/p/6037579.html" target="_blank" rel="noopener">《Http 请求中 Content-Type 和 Accept 讲解以及在 Spring MVC 中的应用》</a> 文章，更加详细。简单的说，<code>consumes</code> 指定请求的内容类型， <code>produces</code>指定返回的结果类型</p></blockquote></li></ul><p>考虑到让开发更加方便，Spring 给每种请求方法提供了对应的注解：</p><ul><li><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java" target="_blank" rel="noopener"><code>@GetMapping</code></a> 注解：对应 <code>@GET</code> 请求方法的 <code>@RequestMapping</code> 注解。</li><li><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/PostMapping.java" target="_blank" rel="noopener"><code>@PostMapping</code></a> 注解：对应 <code>@POST</code> 请求方法的 <code>@RequestMapping</code> 注解。</li><li><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/PutMapping.java" target="_blank" rel="noopener"><code>@PutMapping</code></a> 注解：对应 <code>@PUT</code> 请求方法的 <code>@RequestMapping</code> 注解。</li><li><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/DeleteMapping.java" target="_blank" rel="noopener"><code>@DeleteMapping</code></a> 注解：对应 <code>@DELETE</code> 请求方法的 <code>@RequestMapping</code> 注解。</li><li>还有其它几个，就不一一列举了。</li></ul><h3 id="2-1-3-RequestParam"><a href="#2-1-3-RequestParam" class="headerlink" title="2.1.3 @RequestParam"></a>2.1.3 @RequestParam</h3><p><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestParam.java" target="_blank" rel="noopener"><code>@RequestParam</code></a> 注解，添加在方法参数上，标记该方法参数对应的请求参数的信息。属性如下：</p><ul><li><code>name</code> 属性：对应的请求参数名。如果为空，则直接使用方法上的参数变量名。</li><li><code>value</code> 属性：和 <code>name</code> 属性相同，是它的别名。</li><li><code>required</code> 属性：参数是否必须传。默认为 <code>true</code> ，表示必传。</li><li><code>defaultValue</code> 属性：参数默认值。</li></ul><p><a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/PathVariable.java" target="_blank" rel="noopener"><code>@PathVariable</code></a> 注解，添加在方法参数上，标记接口路径和方法参数的映射关系。具体的，我们在示例中来看。相比 <code>@RequestParam</code> 注解，少一个 <code>defaultValue</code> 属性。</p><p>下面，让我们快速编写一个 SpringMVC 的示例。</p><h2 id="2-2-引入依赖"><a href="#2-2-引入依赖" class="headerlink" title="2.2 引入依赖"></a><span id="anchor-2.2">2.2 引入依赖</span></h2><p>在 <code>pom.xml</code> 文件中，引入相关依赖。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">project</span> <span class="attr">xmlns</span>=<span class="string">"http://maven.apache.org/POM/4.0.0"</span></span></span><br><span class="line"><span class="tag">         <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span></span><br><span class="line"><span class="tag">         <span class="attr">xsi:schemaLocation</span>=<span class="string">"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">parent</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--父级pom指定了springboot的版本--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>labs-springboot2-springmvc<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.javadog<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0-SNAPSHOT<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">parent</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">modelVersion</span>&gt;</span>4.0.0<span class="tag">&lt;/<span class="name">modelVersion</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>labs-springboot2-springmvc-simple<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!-- 实现对 Spring MVC 的自动化配置 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!-- 方便等会写单元测试 --&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">&lt;!--简化--&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">project</span>&gt;</span></span><br></pre></td></tr></table></figure><p>具体每个依赖的作用，胖友自己认真看下艿艿添加的所有注释噢。</p><h2 id="2-3-Application"><a href="#2-3-Application" class="headerlink" title="2.3 Application"></a><span id="anchor-2.3">2.3 Application</span></h2><p>创建 <code>Application</code> 类，配置 <code>@SpringBootApplication</code> 注解即可。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        SpringApplication.run(Application<span class="class">.<span class="keyword">class</span>, <span class="title">args</span>)</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>先暂时不启动项目。等我们添加好 Controller 。</p><h2 id="2-4-UserController"><a href="#2-4-UserController" class="headerlink" title="2.4 UserController"></a><span id="anchor-2.4">2.4 UserController</span></h2><p>创建 <code>UserController</code> 类。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/users"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查询用户列表</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户列表</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">""</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> List&lt;UserVO&gt; <span class="title">list</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 查询列表</span></span><br><span class="line">        List&lt;UserVO&gt; result = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">1</span>).setUsername(<span class="string">"yudaoyuanma"</span>));</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">2</span>).setUsername(<span class="string">"woshiyutou"</span>));</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">3</span>).setUsername(<span class="string">"chifanshuijiao"</span>));</span><br><span class="line">        <span class="comment">// 返回列表</span></span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/&#123;id&#125;"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserVO <span class="title">get</span><span class="params">(@PathVariable(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 查询并返回用户</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(id).setUsername(<span class="string">"username:"</span> + id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 添加用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> addDTO 添加用户信息 DTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 添加成功的用户编号</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping</span>(<span class="string">""</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Integer <span class="title">add</span><span class="params">(UserAddDTO addDTO)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 插入用户记录，返回编号</span></span><br><span class="line">        Integer returnId = <span class="number">1</span>;</span><br><span class="line">        <span class="comment">// 返回用户编号</span></span><br><span class="line">        <span class="keyword">return</span> returnId;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 更新指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> updateDTO 更新用户信息 DTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 是否修改成功</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PutMapping</span>(<span class="string">"/&#123;id&#125;"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Boolean <span class="title">update</span><span class="params">(@PathVariable(<span class="string">"id"</span>)</span> Integer id, UserUpdateDTO updateDTO) </span>&#123;</span><br><span class="line">        <span class="comment">// 将 id 设置到 updateDTO 中</span></span><br><span class="line">        updateDTO.setId(id);</span><br><span class="line">        <span class="comment">// 更新用户记录</span></span><br><span class="line">        Boolean success = <span class="keyword">true</span>;</span><br><span class="line">        <span class="comment">// 返回更新是否成功</span></span><br><span class="line">        <span class="keyword">return</span> success;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 是否删除成功</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@DeleteMapping</span>(<span class="string">"/&#123;id&#125;"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> Boolean <span class="title">delete</span><span class="params">(@PathVariable(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 删除用户记录</span></span><br><span class="line">        Boolean success = <span class="keyword">false</span>;</span><br><span class="line">        <span class="comment">// 返回是否更新成功</span></span><br><span class="line">        <span class="keyword">return</span> success;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在类上，添加 <code>@RestController</code> 注解，表示直接返回接口结果。默认情况下，使用 JSON 作为序列化方式。</p></li><li><p>在类上，添加 <code>@RequestMapping(&quot;/users&quot;)</code> 注解，表示 UserController 所有接口路径，以 <code>/users</code> 开头。</p></li><li><p><code>#list()</code> 方法，查询用户列表。请求对应 <code>GET /users</code>，请求结果为：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="attr">"id"</span>: <span class="number">1</span>,</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"yudaoyuanma"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="attr">"id"</span>: <span class="number">2</span>,</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"woshiyutou"</span></span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="attr">"id"</span>: <span class="number">3</span>,</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"chifanshuijiao"</span></span><br><span class="line">    &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><ul><li>其中，<a href="https://github.com/yuyong725/labs-springboot2/blob/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-simple/src/main/java/cn/javadog/labs/springboot2/springmvc/vo/UserVO.java" target="_blank" rel="noopener">UserVO</a> 为用户返回 VO 类。</li></ul></li><li><p><code>#get(Integer id)</code> 方法，获得指定用户编号的用户。请求对应 <code>GET /users/{id}</code> 【路径参数】，请求你结果为:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"id"</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">"username"</span>: <span class="string">"username:1"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p><code>#add(UserAddDTO addDTO)</code> 方法，添加用户。请求对应 <code>POST /users</code> ，请求结果为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">1</span><br></pre></td></tr></table></figure><ul><li>因为我们这里返回的是 Integer 类型，对于非 POJO 对象，所以无需使用 JSON 序列化返回。</li><li>其中，<a href="https://github.com/yuyong725/labs-springboot2/blob/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-simple/src/main/java/cn/javadog/labs/springboot2/springmvc/dto/UserAddDTO.java" target="_blank" rel="noopener">UserAddDTO</a> 为用户添加 DTO 类。</li></ul></li><li><p><code>#update(Integer id, UserUpdateDTO updateDTO)</code> 方法，更新指定用户编号的用户。请求对应 <code>PUT /users/{id}</code> 【路径参数】，请求结果为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">true</span><br></pre></td></tr></table></figure><ul><li>其中，<a href="https://github.com/yuyong725/labs-springboot2/blob/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-simple/src/main/java/cn/javadog/labs/springboot2/springmvc/dto/UserUpdateDTO.java" target="_blank" rel="noopener">UserUpdateDTO</a> 为用户更新 DTO 类。</li></ul></li><li><p><code>#delete(Integer id)</code> 方法，删除指定用户编号的用户。请求对应 <code>DELETE /users/{id}</code> 【路径参数】，请求结果为：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">false</span><br></pre></td></tr></table></figure></li></ul><p>以上的测试，肯定需要通过运行 Application ，启动项目。这里，补充下它的启动日志如下：</p><figure class="highlight verilog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">00</span><span class="variable">.671</span>  INFO <span class="number">99493</span> --- [           main] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.Application</span>        : Starting Application on MacBook-Pro-<span class="number">8</span> <span class="keyword">with</span> PID <span class="number">99493</span> (/Users/yunai/Java/SpringBoot-Labs/lab-<span class="number">23</span>/lab-springmvc-<span class="number">23</span>-<span class="number">01</span>/target/classes started by yunai in /Users/yunai/Java/SpringBoot-Labs)</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">00</span><span class="variable">.673</span>  INFO <span class="number">99493</span> --- [           main] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.Application</span>        : No active profile set, falling back to <span class="keyword">default</span> profiles: <span class="keyword">default</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.593</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.s</span><span class="variable">.b</span><span class="variable">.w</span><span class="variable">.embedded</span><span class="variable">.tomcat</span><span class="variable">.TomcatWebServer</span>  : Tomcat initialized <span class="keyword">with</span> port(s): <span class="number">8080</span> (http)</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.613</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.apache</span><span class="variable">.catalina</span><span class="variable">.core</span><span class="variable">.StandardService</span>   : Starting service [Tomcat]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.613</span>  INFO <span class="number">99493</span> --- [           main] org<span class="variable">.apache</span><span class="variable">.catalina</span><span class="variable">.core</span><span class="variable">.StandardEngine</span>  : Starting Servlet engine: [Apache Tomcat/<span class="number">9</span><span class="variable">.0</span><span class="variable">.16</span>]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.619</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.a</span><span class="variable">.catalina</span><span class="variable">.core</span><span class="variable">.AprLifecycleListener</span>   : The APR based Apache Tomcat Native <span class="keyword">library</span> which allows optimal performance in production environments was <span class="keyword">not</span> found on the java<span class="variable">.library</span><span class="variable">.path</span>: [/Users/yunai/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:.]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.684</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.a</span><span class="variable">.c</span><span class="variable">.c</span><span class="variable">.C</span>.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.684</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.s</span><span class="variable">.web</span><span class="variable">.context</span><span class="variable">.ContextLoader</span>            : Root WebApplicationContext: initialization completed in <span class="number">976</span> ms</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.844</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.s</span><span class="variable">.s</span><span class="variable">.concurrent</span><span class="variable">.ThreadPoolTaskExecutor</span>  : Initializing ExecutorService 'applicationTaskExecutor'</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.987</span>  INFO <span class="number">99493</span> --- [           main] o<span class="variable">.s</span><span class="variable">.b</span><span class="variable">.w</span><span class="variable">.embedded</span><span class="variable">.tomcat</span><span class="variable">.TomcatWebServer</span>  : Tomcat started on port(s): <span class="number">8080</span> (http) <span class="keyword">with</span> <span class="keyword">context</span> path ''</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">15</span> <span class="number">18</span>:<span class="number">46</span>:<span class="number">01</span><span class="variable">.990</span>  INFO <span class="number">99493</span> --- [           main] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.Application</span>        : Started Application in <span class="number">1</span><span class="variable">.559</span> seconds (JVM running <span class="keyword">for</span> <span class="number">2</span><span class="variable">.146</span>)</span><br></pre></td></tr></table></figure><ul><li>我们可以看到，Spring Boot 在启动 SpringMVC 时，会默认初始化一个内嵌的 Tomcat ，监听 8080 端口的请求。</li></ul><h2 id="2-5-UserController2"><a href="#2-5-UserController2" class="headerlink" title="2.5 UserController2"></a>2.5 UserController2</h2><p>在日常的项目开发中，艿艿只使用 <code>GET</code> 和 <code>POST</code> 请求方法。主要是，实际场景下，因为业务比较复杂，标准的 Restful API 并不能满足所有的操作。例如说，订单有用户取消，管理员取消，修改收货地址，评价等等操作。所以，我们更多的是，提供<strong>类</strong> Restful API 。</p><p>对于 SpringMVC 提供的 <code>@PathVariable</code> 路径参数，艿艿目前也并没有在项目中使用，主要原因如下：</p><ul><li>1、封装的权限框架，基于 URL 作为权限标识，暂时是不支持带有路径参数的 URL 。</li><li>2、基于 URL 进行告警，而带有路径参数的 URL ，“相同” URL 实际对应的是不同的 URL ，导致无法很方便的实现按照单位时间请求错误次数告警。</li><li>3、<code>@PathVariable</code> 路径参数的 URL ，会带来一定的 SpringMVC 的性能下滑。具体可以看看 <a href="https://tech.imdada.cn/2015/12/23/springmvc-restful-optimize/" target="_blank" rel="noopener">《SpringMVC RESTful 性能优化》</a> 文章。</li></ul><p>所以，我们创建 <code>UserController2</code> 类，修改 API 接口。最终代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController2.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/users2"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserController2</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 查询用户列表</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户列表</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/list"</span>) <span class="comment">// URL 修改成 /list</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> List&lt;UserVO&gt; <span class="title">list</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 查询列表</span></span><br><span class="line">        List&lt;UserVO&gt; result = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">1</span>).setUsername(<span class="string">"yudaoyuanma"</span>));</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">2</span>).setUsername(<span class="string">"woshiyutou"</span>));</span><br><span class="line">        result.add(<span class="keyword">new</span> UserVO().setId(<span class="number">3</span>).setUsername(<span class="string">"chifanshuijiao"</span>));</span><br><span class="line">        <span class="comment">// 返回列表</span></span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/get"</span>) <span class="comment">// URL 修改成 /get</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserVO <span class="title">get</span><span class="params">(@RequestParam(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 查询并返回用户</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(id).setUsername(UUID.randomUUID().toString());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 添加用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> addDTO 添加用户信息 DTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 添加成功的用户编号</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping</span>(<span class="string">"add"</span>) <span class="comment">// URL 修改成 /add</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Integer <span class="title">add</span><span class="params">(UserAddDTO addDTO)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 插入用户记录，返回编号</span></span><br><span class="line">        Integer returnId = UUID.randomUUID().hashCode();</span><br><span class="line">        <span class="comment">// 返回用户编号</span></span><br><span class="line">        <span class="keyword">return</span> returnId;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 更新指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> updateDTO 更新用户信息 DTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 是否修改成功</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping</span>(<span class="string">"/update"</span>) <span class="comment">// URL 修改成 /update ，RequestMethod 改成 POST</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Boolean <span class="title">update</span><span class="params">(UserUpdateDTO updateDTO)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 更新用户记录</span></span><br><span class="line">        Boolean success = <span class="keyword">true</span>;</span><br><span class="line">        <span class="comment">// 返回更新是否成功</span></span><br><span class="line">        <span class="keyword">return</span> success;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 是否删除成功</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@DeleteMapping</span>(<span class="string">"/delete"</span>) <span class="comment">// URL 修改成 /delete ，RequestMethod 改成 DELETE</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Boolean <span class="title">delete</span><span class="params">(@RequestParam(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 删除用户记录</span></span><br><span class="line">        Boolean success = <span class="keyword">false</span>;</span><br><span class="line">        <span class="comment">// 返回是否更新成功</span></span><br><span class="line">        <span class="keyword">return</span> success;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>每一处的修改，看下 <code>@XXXMapping</code> 注解后的注释说明。</li></ul><h1 id="3-测试接口"><a href="#3-测试接口" class="headerlink" title="3. 测试接口"></a>3. 测试接口</h1><blockquote><p>示例代码对应仓库： <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-simple" target="_blank" rel="noopener">labs-springboot2-springmvc-simple</a> 。</p></blockquote><p>在开发完接口，我们会进行接口的自测。一般情况下，我们先启动项目，然后使用 <a href="https://www.getpostman.com/" target="_blank" rel="noopener">Postman</a>、<a href="https://curl.haxx.se/" target="_blank" rel="noopener">curl</a>、浏览器，手工模拟请求后端 API 接口。</p><p>实际上，SpringMVC 提供了测试框架 <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/servlet/MockMvc.html" target="_blank" rel="noopener">MockMvc</a> ，方便我们快速测试接口。下面，我们对 <a href="#anchor-2.4">「2.4 UserController」</a> 提供的接口，进行下单元测试。也就是说，本小节，我们会继续在 <a href="https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-23/lab-springmvc-23-01" target="_blank" rel="noopener">lab-springmvc-23-01</a> 示例的基础上修改。</p><p>MockMvc 提供了集成测试和单元测试的能力，我们分成 <code>集成测试</code> 和 <code>单元测试</code>来看。如果胖友对测试这块不太了解，可以看看如下两篇文章：</p><ul><li><a href="http://www.iocoder.cn/Fight/A-little-bit-about-Java-unit-testing/?self" target="_blank" rel="noopener">《小谈 Java 单元测试》</a></li><li><a href="http://www.iocoder.cn/Architecture/talk-about-java-unit-test/?self" target="_blank" rel="noopener">《谈谈单元测试》</a></li></ul><h2 id="3-1-集成测试"><a href="#3-1-集成测试" class="headerlink" title="3.1 集成测试"></a>3.1 集成测试</h2><p>创建 <code>UserControllerTest</code> 测试类，我们来测试一下简单的 UserController 的每个操作。核心代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserControllerTest.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">SpringBootTest</span>(<span class="title">classes</span> </span>= Application<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">AutoConfigureMockMvc</span></span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">UserControllerTest</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> MockMvc mvc;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testList</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 查询用户列表</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(<span class="string">"/users"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().json(<span class="string">"[\n"</span> +</span><br><span class="line">                <span class="string">"    &#123;\n"</span> +</span><br><span class="line">                <span class="string">"        \"id\": 1,\n"</span> +</span><br><span class="line">                <span class="string">"        \"username\": \"yudaoyuanma\"\n"</span> +</span><br><span class="line">                <span class="string">"    &#125;,\n"</span> +</span><br><span class="line">                <span class="string">"    &#123;\n"</span> +</span><br><span class="line">                <span class="string">"        \"id\": 2,\n"</span> +</span><br><span class="line">                <span class="string">"        \"username\": \"woshiyutou\"\n"</span> +</span><br><span class="line">                <span class="string">"    &#125;,\n"</span> +</span><br><span class="line">                <span class="string">"    &#123;\n"</span> +</span><br><span class="line">                <span class="string">"        \"id\": 3,\n"</span> +</span><br><span class="line">                <span class="string">"        \"username\": \"chifanshuijiao\"\n"</span> +</span><br><span class="line">                <span class="string">"    &#125;\n"</span> +</span><br><span class="line">                <span class="string">"]"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testGet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 获得指定用户编号的用户</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(<span class="string">"/users/1"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().json(<span class="string">"&#123;\n"</span> +</span><br><span class="line">                <span class="string">"\"id\": 1,\n"</span> +</span><br><span class="line">                <span class="string">"\"username\": \"username:1\"\n"</span> +</span><br><span class="line">                <span class="string">"&#125;"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testAdd</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 获得指定用户编号的用户</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.post(<span class="string">"/users"</span>)</span><br><span class="line">            .param(<span class="string">"username"</span>, <span class="string">"yudaoyuanma"</span>)</span><br><span class="line">            .param(<span class="string">"passowrd"</span>, <span class="string">"nicai"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().string(<span class="string">"1"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testUpdate</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 获得指定用户编号的用户</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.put(<span class="string">"/users/1"</span>)</span><br><span class="line">                .param(<span class="string">"username"</span>, <span class="string">"yudaoyuanma"</span>)</span><br><span class="line">                .param(<span class="string">"passowrd"</span>, <span class="string">"nicai"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().string(<span class="string">"true"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testDelete</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 获得指定用户编号的用户</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.delete(<span class="string">"/users/1"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().string(<span class="string">"false"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>在类上，我们添加了 <code>@AutoConfigureMockMvc</code> 注解，用于自动化配置我们稍后注入的 MockMvc Bean 对象 <code>mvc</code> 。在后续的测试中，我们会看到都是通过 <code>mvc</code> 调用后端 API 接口。而每一次调用后端 API 接口，都会执行<strong>真正的后端逻辑</strong>。因此，整个逻辑，走的是<strong>集成测试</strong>，会启动一个<strong>真实</strong>的 Spring 环境。</li><li>每次 API 接口的请求，都通过 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java" target="_blank" rel="noopener">MockMvcRequestBuilders</a> 来构建。构建完成后，通过 <code>mvc</code> 执行请求，返回 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/main/java/org/springframework/test/web/servlet/ResultActions.java" target="_blank" rel="noopener">ResultActions</a> 结果。</li><li>执行完请求后，通过调用 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/main/java/org/springframework/test/web/servlet/ResultActions.java" target="_blank" rel="noopener">ResultActions</a> 的 <code>andExpect(ResultMatcher matcher)</code> 方法，添加对结果的预期，相当于做断言。如果不符合预期，则会抛出异常，测试不通过。</li></ul><p>另外，ResultActions 还有两个方法：</p><ul><li><code>#andDo(ResultHandler handler)</code> 方法，添加 ResultHandler 结果处理器，例如说调试时打印结果到控制台，人肉看看结果是否正确。</li><li><code>#andReturn()</code> 方法，最后返回相应的 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java" target="_blank" rel="noopener">MvcResult</a> 结果。后续，自己针对 MvcResult 写一些自定义的逻辑。</li></ul><p>例如说，我们将 <code>#testGet()</code> 方法，使用上述介绍的两个，进行补充改写如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserControllerTest.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testGet2</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="comment">// 获得指定用户编号的用户</span></span><br><span class="line">    ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(<span class="string">"/users/1"</span>));</span><br><span class="line">    <span class="comment">// 校验结果</span></span><br><span class="line">    resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">    resultActions.andExpect(MockMvcResultMatchers.content().json(<span class="string">"&#123;\n"</span> +</span><br><span class="line">            <span class="string">"\"id\": 1,\n"</span> +</span><br><span class="line">            <span class="string">"\"username\": \"username:1\"\n"</span> +</span><br><span class="line">            <span class="string">"&#125;"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// &lt;1&gt; 打印结果</span></span><br><span class="line">    resultActions.andDo(MockMvcResultHandlers.print());</span><br><span class="line"></span><br><span class="line">    <span class="comment">// &lt;2&gt; 获得 MvcResult ，后续执行各种自定义逻辑</span></span><br><span class="line">    MvcResult mvcResult = resultActions.andReturn();</span><br><span class="line">    System.out.println(<span class="string">"拦截器数量："</span> + mvcResult.getInterceptors().length);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p><code>&lt;1&gt;</code> 处，打印请求和响应信息。输出如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">MockHttpServletRequest:</span><br><span class="line">      HTTP Method &#x3D; GET</span><br><span class="line">      Request URI &#x3D; &#x2F;users&#x2F;1</span><br><span class="line">       Parameters &#x3D; &#123;&#125;</span><br><span class="line">          Headers &#x3D; []</span><br><span class="line">             Body &#x3D; null</span><br><span class="line">    Session Attrs &#x3D; &#123;&#125;</span><br><span class="line"></span><br><span class="line">Handler:</span><br><span class="line">             Type &#x3D; cn.javadog.labs.springboot2.springmvc.controller.UserController</span><br><span class="line">           Method &#x3D; public cn.javadog.labs.springboot2.springmvc.vo.UserVO cn.javadog.labs.springboot2.springmvc.controller.UserController.get(java.lang.Integer)</span><br><span class="line"></span><br><span class="line">Async:</span><br><span class="line">    Async started &#x3D; false</span><br><span class="line">     Async result &#x3D; null</span><br><span class="line"></span><br><span class="line">Resolved Exception:</span><br><span class="line">             Type &#x3D; null</span><br><span class="line"></span><br><span class="line">ModelAndView:</span><br><span class="line">        View name &#x3D; null</span><br><span class="line">             View &#x3D; null</span><br><span class="line">            Model &#x3D; null</span><br><span class="line"></span><br><span class="line">FlashMap:</span><br><span class="line">       Attributes &#x3D; null</span><br><span class="line"></span><br><span class="line">MockHttpServletResponse:</span><br><span class="line">           Status &#x3D; 200</span><br><span class="line">    Error message &#x3D; null</span><br><span class="line">          Headers &#x3D; [Content-Type:&quot;application&#x2F;json;charset&#x3D;UTF-8&quot;]</span><br><span class="line">     Content type &#x3D; application&#x2F;json;charset&#x3D;UTF-8</span><br><span class="line">             Body &#x3D; &#123;&quot;id&quot;:1,&quot;username&quot;:&quot;username:1&quot;&#125;</span><br><span class="line">    Forwarded URL &#x3D; null</span><br><span class="line">   Redirected URL &#x3D; null</span><br><span class="line">          Cookies &#x3D; []</span><br></pre></td></tr></table></figure></li><li><p><code>&lt;2&gt;</code> 处，获得 MvcResult 后，打印下拦截器的数量。输出如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">拦截器数量：2</span><br></pre></td></tr></table></figure></li></ul><h2 id="3-2-单元测试"><a href="#3-2-单元测试" class="headerlink" title="3.2 单元测试"></a>3.2 单元测试</h2><p>为了更好的展示 SpringMVC 单元测试的示例，我们需要改写 UserController 的代码，让其会依赖 UserService 。修改点如下：</p><ul><li><p>创建 <code>UserService</code> 类。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserService.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserService</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserVO <span class="title">get</span><span class="params">(Integer id)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(id).setUsername(<span class="string">"test"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><ul><li><p>在 <code>UserController</code> 类中，增加 <code>GET /users/v2/{id}</code> 接口，获得指定用户编号的用户。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/v2/&#123;id&#125;"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> UserVO <span class="title">get2</span><span class="params">(@PathVariable(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> userService.get(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>在代码中，我们注入了 UserService Bean 对象 <code>userService</code> ，然后在新增的接口方法中，会调用 <code>UserService#get(Integer id)</code> 方法，获得指定用户编号的用户。</li></ul></li></ul><ul><li>创建 <code>UserControllerTest2</code> 测试类，我们来测试一下简单的 UserController 的新增的这个 API 操作。代码如下：</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserControllerTest2.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RunWith</span>(SpringRunner<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">@<span class="title">WebMvcTest</span>(<span class="title">UserController</span>.<span class="title">class</span>)</span></span><br><span class="line"><span class="class"><span class="title">public</span> <span class="title">class</span> <span class="title">UserControllerTest2</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> MockMvc mvc;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@MockBean</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testGet2</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// Mock UserService 的 get 方法</span></span><br><span class="line">        System.out.println(<span class="string">"before mock:"</span> + userService.get(<span class="number">1</span>)); <span class="comment">// &lt;1.1&gt;</span></span><br><span class="line">        Mockito.when(userService.get(<span class="number">1</span>)).thenReturn(</span><br><span class="line">                <span class="keyword">new</span> UserVO().setId(<span class="number">1</span>).setUsername(<span class="string">"username:1"</span>)); <span class="comment">// &lt;1.2&gt;</span></span><br><span class="line">        System.out.println(<span class="string">"after mock:"</span> + userService.get(<span class="number">1</span>)); <span class="comment">// &lt;1.3&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 查询用户列表</span></span><br><span class="line">        ResultActions resultActions = mvc.perform(MockMvcRequestBuilders.get(<span class="string">"/users/v2/1"</span>));</span><br><span class="line">        <span class="comment">// 校验结果</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.status().isOk()); <span class="comment">// 响应状态码 200</span></span><br><span class="line">        resultActions.andExpect(MockMvcResultMatchers.content().json(<span class="string">"&#123;\n"</span> +</span><br><span class="line">                <span class="string">"    \"id\": 1,\n"</span> +</span><br><span class="line">                <span class="string">"    \"username\": \"username:1\"\n"</span> +</span><br><span class="line">                <span class="string">"&#125;"</span>)); <span class="comment">// 响应结果</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在类上添加 <a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebMvcTest.java" target="_blank" rel="noopener">@WebMvcTest</a> 注解，并且传入的是 UserController 类，表示我们要对 UserController 进行单元测试。</p></li><li><blockquote><p>艿艿：注意上面每个<strong>加粗</strong>的地方！</p></blockquote></li><li><p><code>userService</code> 属性，我们添加了 <a href="https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/mock/mockito/MockBean.html" target="_blank" rel="noopener">@MockBean</a> 注解，实际这里注入的是一个使用 <a href="https://site.mockito.org/" target="_blank" rel="noopener">Mockito</a> 创建的 UserService Mock 代理对象。如下图所示：</p><p><img src="1.png" alt="UserService Mock 代理对象"></p><ul><li><p>UserController 中，也会注入一个 UserService 属性，此时注入的就是该 Mock 出来的 UserService Bean 对象。</p></li><li><p>默认情况下，</p></li><li><p><code>&lt;1.1&gt;</code> 处，我们调用 <code>UserService#get(Integer id)</code> 方法，然后打印返回结果。执行结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">before mock:null</span><br></pre></td></tr></table></figure><p>结果竟然返回的是 <code>null</code> 空。理论来说，此时应该返回一个 <code>id = 1</code> 的 UserVO 对象。实际上，因为此时的 <code>userService</code> 是通过 Mockito 来 Mock 出来的对象，其所有调用它的方法，返回的都是空。</p></li><li><p><code>&lt;1.2&gt;</code> 处，通过 Mockito 进行 Mock <code>userService</code> 的 <code>#get(Integer id)</code> 方法，当传入的 <code>id = 1</code> 方法参数时，返回 <code>id = 1</code> 并且 <code>username = &quot;username:1&quot;</code> 的 UserVO 对象。</p></li><li><p><code>&lt;1.3&gt;</code> 处，再次调用 <code>UserService#get(Integer id)</code> 方法，然后打印返回结果。执行结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">after mock:UserVO(id&#x3D;1, username&#x3D;username:1)</span><br></pre></td></tr></table></figure><ul><li>打印的就是我们 Mock 返回的 UserVO 对象。</li></ul></li></ul></li><li><p>后续，使用 <code>mvc</code> 完成一次后端 API 调用，并进行断言结果是否正确。执行成功，单元测试通过。</p></li></ul><p>可能胖友对单元测试不是很了解，这里在额外推荐一本书 <a href="https://union-click.jd.com/jdc?d=3kDmSB" target="_blank" rel="noopener">《有效的单元测试》</a> 。很薄，周末抽几个小时就能读完。</p><p>如果觉得本小节还不够，可以看看 <a href="https://spring.io/guides/gs/testing-web/" target="_blank" rel="noopener">《Testing the Web Layer》</a> 文章，Spring 关于这块入门的官方文档。</p><h1 id="4-全局统一返回"><a href="#4-全局统一返回" class="headerlink" title="4. 全局统一返回"></a><span id="anchor-4">4. 全局统一返回</span></h1><blockquote><p>示例代码对应仓库：<a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p></blockquote><p>在我们提供后端 API 给前端时，我们需要告前端，这个 API 调用结果是否成功：</p><ul><li>如果<strong>成功</strong>，成功的数据是什么。后续，前端会取数据渲染到页面上。</li><li>如果<strong>失败</strong>，失败的原因是什么。一般，前端会将原因弹出提示给用户。</li></ul><p>这样，我们就需要有统一的返回结果，而不能是每个接口自己定义自己的风格。一般来说，统一的全局返回信息如下：</p><ul><li>成功时，返回<strong>成功的状态码</strong> + <strong>数据</strong>。</li><li>失败时，返回<strong>失败的状态码</strong> + <strong>错误提示</strong>。</li></ul><p>在标准的 RESTful API 的定义，是推荐使用 <a href="https://zh.wikipedia.org/wiki/HTTP状态码" target="_blank" rel="noopener">HTTP 响应状态码</a> 返回状态码。一般来说，我们实践很少这么去做，主要有如下原因：</p><ul><li>业务返回的错误状态码很多，HTTP 响应状态码无法很好的映射。例如说，活动还未开始、订单已取消等等。</li><li>国内开发者对 HTTP 响应状态码不是很了解，可能只知道 200、403、404、500 几种常见的。这样，反倒增加学习成本。</li></ul><p>所以，实际项目在实践时，我们会将状态码放在 Response Body <strong>响应内容</strong>中返回。</p><p>在全局统一返回里，我们至少需要定义三个字段：</p><ul><li><p><code>code</code>：状态码。无论是否成功，必须返回。</p><ul><li>成功时，状态码为 0 。</li><li>失败时，对应业务的错误码。</li></ul><blockquote><p>关于这一块，也有团队实践时，增加了 <code>success</code> 字段，通过 <code>true</code> 和 <code>false</code> 表示成功还是失败。这个看每个团队的习惯吧。卡姆的话，还是偏好基于约定，返回 0 时表示成功。</p></blockquote></li><li><p><code>data</code>：数据。成功时，返回该字段。</p></li><li><p><code>message</code>：错误提示。失败时，返回该字段。</p></li></ul><p>那么，让我们来看两个示例：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 成功响应</span></span><br><span class="line">&#123;</span><br><span class="line">    code: 0,</span><br><span class="line">    data: &#123;</span><br><span class="line">        id: 1,</span><br><span class="line">        username: "yudaoyuanma"</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 失败响应</span></span><br><span class="line">&#123;</span><br><span class="line">    code: 233666,</span><br><span class="line">    message: "徐妈太丑了"</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-1-引入依赖"><a href="#4-1-引入依赖" class="headerlink" title="4.1 引入依赖"></a>4.1 引入依赖</h2><p>在 <a href="#anchor-2.2">「2.2 引入依赖」</a> 一致。</p><h2 id="4-2-Application"><a href="#4-2-Application" class="headerlink" title="4.2 Application"></a>4.2 Application</h2><p>在 <a href="#anchor-2.3">「2.3 Application」</a> 一致。</p><h2 id="4-3-CommonResult"><a href="#4-3-CommonResult" class="headerlink" title="4.3 CommonResult"></a>4.3 CommonResult</h2><p>创建 <code>CommonResult</code> 类，用于全局统一返回。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CommonResult.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommonResult</span>&lt;<span class="title">T</span>&gt; <span class="keyword">implements</span> <span class="title">Serializable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Integer CODE_SUCCESS = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 错误码</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> Integer code;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 错误提示</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String message;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 返回数据</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> T data;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 将传入的 result 对象，转换成另外一个泛型结果的对象</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * 因为 A 方法返回的 CommonResult 对象，不满足调用其的 B 方法的返回，所以需要进行转换。</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> result 传入的 result 对象</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> &lt;T&gt; 返回的泛型</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 新的 CommonResult 对象</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">CommonResult&lt;T&gt; <span class="title">error</span><span class="params">(CommonResult&lt;?&gt; result)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> error(result.getCode(), result.getMessage());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">CommonResult&lt;T&gt; <span class="title">error</span><span class="params">(Integer code, String message)</span> </span>&#123;</span><br><span class="line">        Assert.isTrue(!CODE_SUCCESS.equals(code), <span class="string">"code 必须是错误的！"</span>);</span><br><span class="line">        CommonResult&lt;T&gt; result = <span class="keyword">new</span> CommonResult&lt;&gt;();</span><br><span class="line">        result.code = code;</span><br><span class="line">        result.message = message;</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">CommonResult&lt;T&gt; <span class="title">success</span><span class="params">(T data)</span> </span>&#123;</span><br><span class="line">        CommonResult&lt;T&gt; result = <span class="keyword">new</span> CommonResult&lt;&gt;();</span><br><span class="line">        result.code = CODE_SUCCESS;</span><br><span class="line">        result.data = data;</span><br><span class="line">        result.message = <span class="string">""</span>;</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@JsonIgnore</span> <span class="comment">// 忽略，避免 jackson 序列化给前端</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isSuccess</span><span class="params">()</span> </span>&#123; <span class="comment">// 方便判断是否成功</span></span><br><span class="line">        <span class="keyword">return</span> CODE_SUCCESS.equals(code);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@JsonIgnore</span> <span class="comment">// 忽略，避免 jackson 序列化给前端</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">isError</span><span class="params">()</span> </span>&#123; <span class="comment">// 方便判断是否失败</span></span><br><span class="line">        <span class="keyword">return</span> !isSuccess();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-4-GlobalResponseBodyHandler"><a href="#4-4-GlobalResponseBodyHandler" class="headerlink" title="4.4 GlobalResponseBodyHandler"></a><span id="anchor-4.4">4.4 GlobalResponseBodyHandler</span></h2><p>创建 <code>GlobalResponseBodyHandler</code> 类，全局统一返回的处理器。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// GlobalResponseBodyHandler.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@ControllerAdvice</span>(basePackages = <span class="string">"cn.javadog.labs.springboot2.springmvc.controller"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">GlobalResponseBodyHandler</span> <span class="keyword">implements</span> <span class="title">ResponseBodyAdvice</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">supports</span><span class="params">(MethodParameter returnType, Class converterType)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 返回 true 。表示拦截 Controller 所有 API 接口的返回结果</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 当返回的结果不是 CommonResult 类型时，则包装成 CommonResult 类型</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">beforeBodyWrite</span><span class="params">(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,</span></span></span><br><span class="line"><span class="function"><span class="params">                                  ServerHttpRequest request, ServerHttpResponse response)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 如果已经是 CommonResult 类型，则直接返回</span></span><br><span class="line">        <span class="keyword">if</span> (body <span class="keyword">instanceof</span> CommonResult) &#123;</span><br><span class="line">            <span class="keyword">return</span> body;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 如果不是，则包装成 CommonResult 类型</span></span><br><span class="line">        <span class="keyword">return</span> CommonResult.success(body);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>在 SpringMVC 中，可以使用通过实现 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java" target="_blank" rel="noopener">ResponseBodyAdvice</a> 接口，并添加 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java" target="_blank" rel="noopener">@ControllerAdvice</a> 接口，拦截 Controller 的返回结果。注意，我们这里 <code>@ControllerAdvice</code> 注解，设置了 <code>basePackages</code> 属性，<strong>只拦截</strong> <code>cn.javadog.labs.springboot2.springmvc.controller</code> 包，也就是我们定义的 Controller 。为什么呢？因为在项目中，我们可能会引入 Swagger 等库，也使用 Controller 提供 API 接口，那么我们显然不应该让 GlobalResponseBodyHandler 去拦截这些接口，<strong>毕竟它们并不需要我们去替它们做全局统一的返回</strong>。</li><li>实现 <code>#supports(MethodParameter returnType, Class converterType)</code> 方法，返回 <code>true</code> 。表示拦截 Controller 所有 API 接口的返回结果。</li><li>实现 <code>#beforeBodyWrite(...)</code> 方法，当返回的结果不是 CommonResult 类型时，则包装成 CommonResult 类型。这里有两点要注意：<ul><li>第一点，可能 API 接口的返回结果已经是 CommonResult 类型，就无需做二次包装了。</li><li>第二点，API 接口既然返回结果，被 GlobalResponseBodyHandler 拦截到，<strong>约定</strong>就是成功返回，所以使用 <code>CommonResult#success(T data)</code> 方法，进行包装成<strong>成功</strong>的 CommonResult 返回。那么，如果我们希望 API 接口是<strong>失败</strong>的返回呢？我们<strong>约定</strong>在 Controller 抛出异常，这点我们会在 <a href="#anchor-5">「5. 全局异常处理」</a> 看到。</li></ul></li></ul><h2 id="4-5-UserController"><a href="#4-5-UserController" class="headerlink" title="4.5 UserController"></a>4.5 UserController</h2><p>创建 <code>UserController</code> 类。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/users"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * 提供不使用 CommonResult 包装</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/get"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserVO <span class="title">get</span><span class="params">(@RequestParam(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 查询并返回用户</span></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(id).setUsername(UUID.randomUUID().toString());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * 提供使用 CommonResult 包装</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户编号</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/get2"</span>)</span><br><span class="line">    <span class="function"><span class="keyword">public</span> CommonResult&lt;UserVO&gt; <span class="title">get2</span><span class="params">(@RequestParam(<span class="string">"id"</span>)</span> Integer id) </span>&#123;</span><br><span class="line">        <span class="comment">// 查询用户</span></span><br><span class="line">        UserVO user = <span class="keyword">new</span> UserVO().setId(id).setUsername(UUID.randomUUID().toString());</span><br><span class="line">        <span class="comment">// 返回结果</span></span><br><span class="line">        <span class="keyword">return</span> CommonResult.success(user);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在 <code>#get(Integer id)</code> 方法，返回的结果是 UserVO 类型。这样，结果会被 GlobalResponseBodyHandler 拦截，包装成 CommonResult 类型返回。请求结果如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"code"</span>: <span class="number">0</span>,</span><br><span class="line">    <span class="attr">"message"</span>: <span class="string">""</span>,</span><br><span class="line">    <span class="attr">"data"</span>: &#123;</span><br><span class="line">        <span class="attr">"id"</span>: <span class="number">10</span>,</span><br><span class="line">        <span class="attr">"username"</span>: <span class="string">"f0ab9401-062f-4697-bcc9-1dc70c1c1310"</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>会有 <code>&quot;message&quot;: &quot;&quot;</code> 的返回的原因是，我们使用 SpringMVC 提供的 Jackson 序列化，对于 CommonResult 此时的 <code>message = null</code> 的情况下，会序列化它成 <code>&quot;message&quot;: &quot;&quot;</code> 返回。实际情况下，不会影响前端处理。</li></ul></li><li><p>在 <code>#get2(Integer id)</code> 方法，返回的结果是 Common<uservo>类型。结果虽然也会被 GlobalResponseBodyHandler 拦截，但是<strong>不会</strong>二次再重复包装成 CommonResult 类型返回。</uservo></p></li></ul><h2 id="4-6-小小的讨论"><a href="#4-6-小小的讨论" class="headerlink" title="4.6 小小的讨论"></a>4.6 小小的讨论</h2><p>至此，我们已经完成了全局统一返回的示例。不过呢，卡姆在这里想抛出一个话题？我们是否应该使用 GlobalResponseBodyHandler 这样的方式，拦截 Controller 的返回结果，返回全局统一返回的示例呢？</p><p>我们会发现，网上大量的文章，其实也是使用这样的方式，实现全局统一返回。这样看起来，好像也并没有什么问题。当然，实际在项目中使用，没有问题，没有坑，放放心食用。这里，只是卡姆的一个纠结点。我们在使用 AOP 的时候，修改一个方法的返回<strong>结果的类型</strong>，真的合适吗？其实两种方式，都可以，按照自己的理念即可。</p><p>另外，ResponseBodyAdvice 用于对响应内容 Response Body 进行切面拦截。而相对的，SpringMVC 也提供了 <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.html" target="_blank" rel="noopener">RequestBodyAdvice</a> ，用于对请求内容 Request Body 进行切面拦截。使用比较简单，和 ResponseBodyAdvice 是类似的，就不重复赘述了。当然，貌似 ResponseBodyAdvice 使用也比较少，胖友知道有这么个东西即可，等到有需要的时候，再来瞅瞅。</p><h1 id="5-全局异常处理"><a href="#5-全局异常处理" class="headerlink" title="5. 全局异常处理"></a><span id="anchor-5">5. 全局异常处理</span></h1><blockquote><p>示例代码对应仓库：<a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p></blockquote><p>在 <a href="#anchor-4">「4. 全局统一返回」</a> 中，我们已经定义了使用 CommonResult 全局统一返回，并且看到了成功返回的示例与代码。这一小节，我们主要是来全局异常处理，最终能也是通过 CommonResult 返回。</p><blockquote><p>友情提示：该示例，基于 <a href="#anchor-4">「4. 全局统一返回」</a> 的 <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 的基础上，继续改造。</p></blockquote><h2 id="5-1-ServiceExceptionEnum"><a href="#5-1-ServiceExceptionEnum" class="headerlink" title="5.1 ServiceExceptionEnum"></a>5.1 ServiceExceptionEnum</h2><p>创建 <code>ServiceExceptionEnum</code> 枚举类，枚举项目中的错误码。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ServiceExceptionEnum.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> ServiceExceptionEnum &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ========== 系统级别 ==========</span></span><br><span class="line">    SUCCESS(<span class="number">0</span>, <span class="string">"成功"</span>),</span><br><span class="line">    SYS_ERROR(<span class="number">2001001000</span>, <span class="string">"服务端发生异常"</span>),</span><br><span class="line">    MISSING_REQUEST_PARAM_ERROR(<span class="number">2001001001</span>, <span class="string">"参数缺失"</span>),</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ========== 用户模块 ==========</span></span><br><span class="line">    USER_NOT_FOUND(<span class="number">1001002000</span>, <span class="string">"用户不存在"</span>),</span><br><span class="line"></span><br><span class="line">    ;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 错误码</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> code;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 错误提示</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> String message;</span><br><span class="line"></span><br><span class="line">    ServiceExceptionEnum(<span class="keyword">int</span> code, String message) &#123;</span><br><span class="line">        <span class="keyword">this</span>.code = code;</span><br><span class="line">        <span class="keyword">this</span>.message = message;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ... 省略 getting 方法</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>因为错误码是全局的，最好按照模块来拆分。如下是「芋道源码」在 <a href="https://github.com/YunaiV/onemall" target="_blank" rel="noopener">onemall</a> 项目的实践：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">&#x2F;**</span><br><span class="line"> * 服务异常</span><br><span class="line"> *</span><br><span class="line"> * 参考 https:&#x2F;&#x2F;www.kancloud.cn&#x2F;onebase&#x2F;ob&#x2F;484204 文章</span><br><span class="line"> *</span><br><span class="line"> * 一共 10 位，分成四段</span><br><span class="line"> *</span><br><span class="line"> * 第一段，1 位，类型</span><br><span class="line"> *      1 - 业务级别异常</span><br><span class="line"> *      2 - 系统级别异常</span><br><span class="line"> * 第二段，3 位，系统类型</span><br><span class="line"> *      001 - 用户系统</span><br><span class="line"> *      002 - 商品系统</span><br><span class="line"> *      003 - 订单系统</span><br><span class="line"> *      004 - 支付系统</span><br><span class="line"> *      005 - 优惠劵系统</span><br><span class="line"> *      ... - ...</span><br><span class="line"> * 第三段，3 位，模块</span><br><span class="line"> *      不限制规则。</span><br><span class="line"> *      一般建议，每个系统里面，可能有多个模块，可以再去做分段。以用户系统为例子：</span><br><span class="line"> *          001 - OAuth2 模块</span><br><span class="line"> *          002 - User 模块</span><br><span class="line"> *          003 - MobileCode 模块</span><br><span class="line"> * 第四段，3 位，错误码</span><br><span class="line"> *       不限制规则。</span><br><span class="line"> *       一般建议，每个模块自增。</span><br><span class="line"> *&#x2F;</span><br></pre></td></tr></table></figure></li></ul><h2 id="5-2-ServiceException"><a href="#5-2-ServiceException" class="headerlink" title="5.2 ServiceException"></a>5.2 ServiceException</h2><p>我们在一起讨论下 Service 逻辑异常的时候，如何进行返回。这里的逻辑异常，我们指的是，例如说用户名已经存在，商品库存不足等。一般来说，常用的方案选择，有两种：</p><ul><li>封装统一的业务异常类 ServiceException ，里面有错误码和错误提示，然后进行 <code>throws</code> 抛出。</li><li>封装通用的返回类 CommonResult ，里面有错误码和错误提示，然后进行 <code>return</code> 返回。</li></ul><p>一开始，我们选择了 CommonResult ，结果发现如下情况：</p><ul><li>因为 Spring <code>@Transactional</code> 声明式事务，是基于异常进行回滚的，如果使用 CommonResult 返回，则事务回滚会非常麻烦。</li><li>当调用别的方法时，如果别人返回的是 CommonResult 对象，还需要不断的进行判断，写起来挺麻烦的。</li></ul><p>所以，后来我们采用了抛出业务异常 ServiceException 的方式。</p><p>创建 <code>ServiceException</code> 异常类，继承 RuntimeException 异常类，用于定义业务异常。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ServiceException.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">ServiceException</span> <span class="keyword">extends</span> <span class="title">RuntimeException</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 错误码</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Integer code;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ServiceException</span><span class="params">(ServiceExceptionEnum serviceExceptionEnum)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 使用父类的 message 字段</span></span><br><span class="line">        <span class="keyword">super</span>(serviceExceptionEnum.getMessage());</span><br><span class="line">        <span class="comment">// 设置错误码</span></span><br><span class="line">        <span class="keyword">this</span>.code = serviceExceptionEnum.getCode();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ... 省略 getting 方法</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>提供传入 <code>serviceExceptionEnum</code> 参数的构造方法。具体的处理，看下代码和注释。</li></ul><h2 id="5-3-GlobalExceptionHandler"><a href="#5-3-GlobalExceptionHandler" class="headerlink" title="5.3 GlobalExceptionHandler"></a>5.3 GlobalExceptionHandler</h2><p>创建 <code>GlobalExceptionHandler</code> 类，全局统一返回的处理器。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// GlobalExceptionHandler.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@ControllerAdvice</span>(basePackages = <span class="string">"cn.iocoder.springboot.lab23.springmvc.controller"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">GlobalExceptionHandler</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理 ServiceException 异常</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@ResponseBody</span></span><br><span class="line">    <span class="meta">@ExceptionHandler</span>(value = ServiceException<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">    <span class="title">public</span> <span class="title">CommonResult</span> <span class="title">serviceExceptionHandler</span>(<span class="title">HttpServletRequest</span> <span class="title">req</span>, <span class="title">ServiceException</span> <span class="title">ex</span>) </span>&#123;</span><br><span class="line">        logger.debug(<span class="string">"[serviceExceptionHandler]"</span>, ex);</span><br><span class="line">        <span class="comment">// 包装 CommonResult 结果</span></span><br><span class="line">        <span class="keyword">return</span> CommonResult.error(ex.getCode(), ex.getMessage());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理 MissingServletRequestParameterException 异常</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * SpringMVC 参数不正确</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@ResponseBody</span></span><br><span class="line">    <span class="meta">@ExceptionHandler</span>(value = MissingServletRequestParameterException<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">    <span class="title">public</span> <span class="title">CommonResult</span> <span class="title">missingServletRequestParameterExceptionHandler</span>(<span class="title">HttpServletRequest</span> <span class="title">req</span>, <span class="title">MissingServletRequestParameterException</span> <span class="title">ex</span>) </span>&#123;</span><br><span class="line">        logger.debug(<span class="string">"[missingServletRequestParameterExceptionHandler]"</span>, ex);</span><br><span class="line">        <span class="comment">// 包装 CommonResult 结果</span></span><br><span class="line">        <span class="keyword">return</span> CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),</span><br><span class="line">                ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理其它 Exception 异常</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@ResponseBody</span></span><br><span class="line">    <span class="meta">@ExceptionHandler</span>(value = Exception<span class="class">.<span class="keyword">class</span>)</span></span><br><span class="line"><span class="class">    <span class="title">public</span> <span class="title">CommonResult</span> <span class="title">exceptionHandler</span>(<span class="title">HttpServletRequest</span> <span class="title">req</span>, <span class="title">Exception</span> <span class="title">e</span>) </span>&#123;</span><br><span class="line">        <span class="comment">// 记录异常日志</span></span><br><span class="line">        logger.error(<span class="string">"[exceptionHandler]"</span>, e);</span><br><span class="line">        <span class="comment">// 返回 ERROR CommonResult</span></span><br><span class="line">        <span class="keyword">return</span> CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),</span><br><span class="line">                ServiceExceptionEnum.SYS_ERROR.getMessage());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>在类上，添加 <code>@ControllerAdvice</code> 注解。这一点，和 <a href="#anchor-4.4">「4.4 GlobalResponseBodyHandler」</a> 是一样的。不过，不会实现 ResponseBodyAdvice 接口，因为咱不需要拦截接口返回结果，进行修改。</li><li>我们定义了三个方法，通过添加 <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html" target="_blank" rel="noopener">@ExceptionHandler</a> 注解，定义每个方法对应处理的异常。并且，也添加了 <code>@ResponseBody</code> 注解，标记直接使用返回结果作为 API 的响应。</li><li><code>#serviceExceptionHandler(...)</code> 方法，拦截处理 ServiceException 业务异常，直接使用该异常的 <code>code</code> + <code>message</code> 属性，构建出 CommonResult 对象返回。</li><li><code>#missingServletRequestParameterExceptionHandler(...)</code> 方法，拦截处理 MissingServletRequestParameterException 请求参数异常，构建出错误码为 <code>ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR</code> 的 CommonResult 对象返回。</li><li><code>#exceptionHandler(...)</code> 方法，拦截处理 Exception 异常，构建出错误码为 <code>ServiceExceptionEnum.SYS_ERROR</code> 的 CommonResult 对象返回。这是一个<strong>兜底</strong>的异常处理，避免有一些其它异常，我们没有在 GlobalExceptionHandler 中，提供自定义的处理方式。</li></ul><p><strong>注意</strong>，在 <code>#exceptionHandler(...)</code> 方法中，我们还多使用 <code>logger</code> 打印了错误日志，方便我们接入 ELK 等日志服务，发起告警，通知我们去排查解决。如果胖友的系统里暂时没有日志服务，可以记录错误日志到数据库中，也是不错的选择。而其它两个方法，因为是更偏业务的，相对正常的异常，所以无需记录错误日志。</p><h2 id="5-4-UserController"><a href="#5-4-UserController" class="headerlink" title="5.4 UserController"></a>5.4 UserController</h2><p>在 <code>UserController</code> ，我们添加两个 API 接口，抛出异常，方便我们测试全局异常处理的效果。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 测试抛出 NullPointerException 异常</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/exception-01"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> UserVO <span class="title">exception01</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> NullPointerException(<span class="string">"没有粗面鱼丸"</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 测试抛出 ServiceException 异常</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/exception-02"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> UserVO <span class="title">exception02</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在 <code>#exception01()</code> 方法，抛出 NullPointerException 异常。这样，异常会被 <code>GlobalExceptionHandler#exceptionHandler(...)</code> 方法来拦截，包装成 CommonResult 类型返回。请求结果如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"code"</span>: <span class="number">2001001000</span>,</span><br><span class="line">    <span class="attr">"message"</span>: <span class="string">"服务端发生异常"</span>,</span><br><span class="line">    <span class="attr">"data"</span>: <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>在 <code>#exception02()</code> 方法，抛出 ServiceException 异常。这样，异常会被 <code>GlobalExceptionHandler#serviceExceptionHandler(...)</code> 方法来拦截，包装成 CommonResult 类型返回。请求结果如下：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"code"</span>: <span class="number">1001002000</span>,</span><br><span class="line">    <span class="attr">"message"</span>: <span class="string">"用户不存在"</span>,</span><br><span class="line">    <span class="attr">"data"</span>: <span class="literal">null</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><h1 id="6-HandlerInterceptor-拦截器"><a href="#6-HandlerInterceptor-拦截器" class="headerlink" title="6. HandlerInterceptor 拦截器"></a><span id="anchor-6">6. HandlerInterceptor 拦截器</span></h1><blockquote><p>示例代码对应仓库：<a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p></blockquote><p>在使用 SpringMVC 的时候，我们可以使用 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java" target="_blank" rel="noopener">HandlerInterceptor</a> ，拦截 SpringMVC 处理请求的过程，自定义前置和处理的逻辑。例如说：</p><ul><li>日志拦截器，记录请求与响应。这样，我们可以知道每一次请求的参数，响应的结果，执行的时长等等信息。</li><li>认证拦截器，我们可以解析前端传入的用户标识，例如说 <code>access_token</code> 访问令牌，获得当前用户的信息，记录到 ThreadLocal 中。这样，后续的逻辑，只需要通过 ThreadLocal 就可以获取到用户信息。</li><li>授权拦截器，我们可以通过每个 API 接口需要的授权信息，进行判断，当前请求是否允许访问。例如说，用户是否登陆，是否有该 API 操作的权限等等。</li><li>限流拦截器，我们可以通过每个 API 接口的限流配置，进行判断，当前请求是否超过允许的请求频率，避免恶意的请求，打爆整个系统。</li></ul><p>HandlerInterceptor 接口，定义了三个拦截点。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// HandlerInterceptor.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">HandlerInterceptor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">	<span class="function"><span class="keyword">default</span> <span class="keyword">boolean</span> <span class="title">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span></span></span><br><span class="line"><span class="function">			<span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">		<span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">postHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler,</span></span></span><br><span class="line"><span class="function"><span class="params">			@Nullable ModelAndView modelAndView)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="function"><span class="keyword">default</span> <span class="keyword">void</span> <span class="title">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler,</span></span></span><br><span class="line"><span class="function"><span class="params">			@Nullable Exception ex)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>首先，我们要普及一个概念。我们的每一个 API 请求，会对应到一个 <code>handler</code> 处理器。如下图所示：<img src="2.png" alt="img"></p><ul><li>我们可以看到，这一次 <code>users/exception_03</code> 请求，<code>handler</code> 对应上了 UserController 的 <code>#exception03()</code> 方法。</li><li>也因此，HandlerInterceptor 的接口名上，是以 <strong>Handler</strong> 开头，基于 Handler 的拦截器。</li></ul></li><li><p>然后，我们先来看一段伪代码，看看这三个拦截点和 <code>handler</code> 的执行过程，是怎么结合的。代码如下：</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 伪代码</span></span><br><span class="line">Exception ex = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="comment">// 前置处理</span></span><br><span class="line">    <span class="keyword">if</span> (!preHandle(request, response, handler)) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 执行处理器，即执行 API 的逻辑</span></span><br><span class="line">    handler.execute();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 后置处理</span></span><br><span class="line">    postHandle(request, response, handler);</span><br><span class="line">&#125; <span class="keyword">catch</span>(Exception exception) &#123;</span><br><span class="line">    <span class="comment">// 如果发生了异常，记录到 ex 中</span></span><br><span class="line">    ex = exception;</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">    afterCompletion(request, response, handler);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>结合代码注释，胖友理解下整个处理的过程。</li><li>当然，这里仅仅是伪代码，并没有考虑多个拦截器的情况下。我们会在下文中，提供具体的代码示例，方便胖友更加深入的理解整个执行过程。</li></ul><ul><li><p>例如说，判断用户是否已经登录，如果未登录，返回 <code>false</code> ，<strong>不进行</strong>后续 <code>handler</code> 的执行。</p></li><li><p>例如说，在视图 View 在渲染之前，做一些处理。不过因为目前都前后端分离，所以这个后置拦截点，使用的就已经比较少了。</p></li><li><p>例如说，释放资源。比如，清理认证拦截器产生的 ThreadLocal 线程变量，避免“污染”下一个使用到该线程的请求。</p></li><li><p>又例如说，处理 <code>handler</code> 执行过程中发生的异常，并记录异常日志。不过因为现在一般通过 <a href="#anchor-5">「5. 全局异常处理」</a> 来处理，所以很少这么做了。</p></li><li><p>再例如说，记录请求结束时间，这样我们就可以计算出整个请求的耗时。</p></li></ul><p>在上文中，考虑到让 HandlerInterceptor 的理解更加简洁，一直漏说了一个概念，多个 HandlerInterceptor 们，可以组成一个 Chain <strong>拦截器链</strong>。那么，整个执行的过程，就变成：</p><ul><li>首先，按照 HandlerInterceptor 链的<strong>正序</strong>，执行 <code>#preHandle(...)</code> 方法。</li><li>然后，执行 <code>handler</code> 的逻辑处理。</li><li>之后，按照 HandlerInterceptor 链的<strong>倒序</strong>，执行 <code>#postHandle(...)</code> 方法。</li><li>最后，按照 HandlerInterceptor 链的<strong>倒序</strong>，执行 <code>#afterCompletion(...)</code> 方法。</li></ul><p>这里，我们先只说了<strong>正常</strong>执行的情况。那么<strong>异常</strong>执行的情况呢？例如说：</p><ul><li>某个 HandlerInterceptor 执行 <code>#preHandle(...)</code> 方法，返回 <code>false</code> 的情况。</li><li><code>handler</code> 的逻辑处理，抛出 Exception 异常的情况。</li><li>某个 HandlerInterceptor 执行 <code>#afterCompletion(...)</code> 方法，抛出 Exception 异常的情况。</li><li>… 等等</li></ul><blockquote><p>友情提示：该示例，基于 <a href="#anchor-5">「5. 全局异常处理」</a> 的 <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 的基础上，继续改造。</p></blockquote><h2 id="6-1-自定义-HandlerInterceptor"><a href="#6-1-自定义-HandlerInterceptor" class="headerlink" title="6.1 自定义 HandlerInterceptor"></a>6.1 自定义 HandlerInterceptor</h2><p>我们创建三个自定义 HandlerInterceptor 拦截器。</p><ul><li><p><code>FirstInterceptor</code> 代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// FirstInterceptor.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">FirstInterceptor</span> <span class="keyword">implements</span> <span class="title">HandlerInterceptor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[preHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[postHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[afterCompletion][handler(&#123;&#125;)]"</span>, handler, ex);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>每个方法中，打印日志。</li></ul></li></ul><ul><li><p><code>SecondInterceptor</code> 代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SecondInterceptor.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SecondInterceptor</span> <span class="keyword">implements</span> <span class="title">HandlerInterceptor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[preHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>; <span class="comment">// 故意返回 false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[postHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[afterCompletion][handler(&#123;&#125;)]"</span>, handler, ex);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>和 <code>FirstInterceptor</code> 基本一致，差别在于 <code>#preHandle(...)</code> 方法，返回 <code>false</code> 。</li></ul></li></ul><ul><li><p><code>ThirdInterceptor</code> 代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ThirdInterceptor.java</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ThirdInterceptor</span> <span class="keyword">implements</span> <span class="title">HandlerInterceptor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[preHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">postHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[postHandle][handler(&#123;&#125;)]"</span>, handler);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterCompletion</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[afterCompletion][handler(&#123;&#125;)]"</span>, handler, ex);</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"故意抛个错误"</span>); <span class="comment">// 故意抛出异常</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>和 <code>FirstInterceptor</code> 基本一致，差别在于 <code>#afterCompletion(...)</code> 方法，抛出 RuntimeException 异常。</li></ul></li></ul><h2 id="6-2-SpringMVCConfiguration"><a href="#6-2-SpringMVCConfiguration" class="headerlink" title="6.2 SpringMVCConfiguration"></a>6.2 SpringMVCConfiguration</h2><p>创建 <code>SpringMVCConfiguration</code> 配置类。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringMVCConfiguration</span> <span class="keyword">implements</span> <span class="title">WebMvcConfigurer</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> FirstInterceptor <span class="title">firstInterceptor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> FirstInterceptor();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> SecondInterceptor <span class="title">secondInterceptor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> SecondInterceptor();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> ThirdInterceptor <span class="title">thirdInterceptor</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> ThirdInterceptor();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 拦截器一</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">this</span>.firstInterceptor()).addPathPatterns(<span class="string">"/**"</span>);</span><br><span class="line">        <span class="comment">// 拦截器二</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">this</span>.secondInterceptor()).addPathPatterns(<span class="string">"/users/current_user"</span>);</span><br><span class="line">        <span class="comment">// 拦截器三</span></span><br><span class="line">        registry.addInterceptor(<span class="keyword">this</span>.thirdInterceptor()).addPathPatterns(<span class="string">"/**"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>该配置类，实现 <a href="https://www.jianshu.com/p/52f39b799fbb" target="_blank" rel="noopener">WebMvcConfigurer</a> 接口，实现 SpringMVC 的自定义配置。当然，类上还是要加上 <code>@Configuration</code> 注解，表明 SpringMVCConfiguration 是个配置类。</p></li><li><p><code>#addInterceptors(InterceptorRegistry registry)</code> 方法，添加自定义的 HandlerInterceptor 拦截器，到 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/InterceptorRegistry.java" target="_blank" rel="noopener">InterceptorRegistry</a> 拦截器注册表中。其中，SecondInterceptor 拦截器，我们配置拦截的是 <code>/users/current_user</code> 路径，用于测试 <code>SecondInterceptor#preHandle(...)</code> 前置拦截返回 <code>false</code> 的情况。</p></li></ul><h2 id="6-3-UserController"><a href="#6-3-UserController" class="headerlink" title="6.3 UserController"></a>6.3 UserController</h2><p>本小节，我们会在 <code>UserController</code> 接口，测试拦截器链的表现。</p><p><strong>① <code>/users/do_something</code> 接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/do_something"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    logger.info(<span class="string">"[doSomething]"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用该接口，执行日志如下：</p><figure class="highlight verilog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先，按照 HandlerInterceptor 链的**正序**，执行 `#preHandle(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.049</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [preHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.050</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.ThirdInterceptor</span>           : [preHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.055</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.controller</span><span class="variable">.UserController</span>      :</span><br><span class="line"></span><br><span class="line"><span class="comment">// 然后，执行 `handler` 的逻辑处理。</span></span><br><span class="line">[doSomething]</span><br><span class="line"></span><br><span class="line"><span class="comment">// 之后，按照 HandlerInterceptor 链的**倒序**，执行 `#postHandle(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.109</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.ThirdInterceptor</span>           : [postHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.109</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [postHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br><span class="line"></span><br><span class="line"><span class="comment">// 最后，按照 HandlerInterceptor 链的**倒序**，执行 `#afterCompletion(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.109</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.ThirdInterceptor</span>           : [afterCompletion][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br><span class="line">java<span class="variable">.lang</span><span class="variable">.RuntimeException</span>: 故意抛个错误 <span class="comment">// ... 省略异常堆栈</span></span><br><span class="line"></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">31</span>:<span class="number">38</span><span class="variable">.116</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">1</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [afterCompletion][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.doSomething</span>())]</span><br></pre></td></tr></table></figure><ul><li>整个执行日志，看下添加在其上的注释。</li><li>因为 SecondInterceptor 拦截的是 <code>/users/current_user</code> 路径，所以它没有拦截本次 API 调用。</li><li><strong>注意</strong>，虽然说，ThirdInterceptor 在 <code>#afterCompletion(...)</code> 方法中，抛出异常，但是<strong>不影响</strong> FirstInterceptor 的 <code>#afterCompletion(...)</code> 的后续执行。</li></ul><p>② <strong><code>/users/current_user</code> 接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/current_user"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> UserVO <span class="title">currentUser</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    logger.info(<span class="string">"[currentUser]"</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(<span class="number">10</span>).setUsername(UUID.randomUUID().toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用该接口，执行日志如下：</p><figure class="highlight verilog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先，按照 HandlerInterceptor 链的**正序**，执行 `#preHandle(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">48</span>:<span class="number">37</span><span class="variable">.357</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">5</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [preHandle][handler(public cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.vo</span><span class="variable">.UserVO</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.currentUser</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">48</span>:<span class="number">37</span><span class="variable">.357</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">5</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.SecondInterceptor</span>          : [preHandle][handler(public cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.vo</span><span class="variable">.UserVO</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.currentUser</span>())]</span><br><span class="line"></span><br><span class="line"><span class="comment">//【不存在】然后，执行 `handler` 的逻辑处理。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//【不存在】之后，按照 HandlerInterceptor 链的**倒序**，执行 `#postHandle(...)` 方法。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 最后，按照 HandlerInterceptor 链的**倒序**，执行 `#afterCompletion(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">48</span>:<span class="number">37</span><span class="variable">.358</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">5</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [afterCompletion][handler(public cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.vo</span><span class="variable">.UserVO</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.currentUser</span>())]</span><br></pre></td></tr></table></figure><ul><li>因为只有 FirstInterceptor <strong>完成</strong>了 <code>#preHandle(...)</code> 方法的执行，所以也只有 FirstInterceptor 的 <code>#afterCompletion(...)</code> 方法被执行。</li><li>在 <code>handler</code> 未执行逻辑处理的情况下，HandlerInterceptor 的 <code>#postHandle(...)</code> 方法不会执行。答案也很显而易见，<code>handler</code> 丫根就没完成执行，何来的后置处理，嘿嘿。</li></ul><p><strong>③ <code>/users/exception-03</code> 接口</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping</span>(<span class="string">"/exception-03"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">exception03</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    logger.info(<span class="string">"[exception03]"</span>);</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调用该接口，执行日志如下：</p><figure class="highlight verilog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 首先，按照 HandlerInterceptor 链的**正序**，执行 `#preHandle(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.029</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [preHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.exception03</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.029</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.ThirdInterceptor</span>           : [preHandle][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.exception03</span>())]</span><br><span class="line"></span><br><span class="line"><span class="comment">// 然后，执行 `handler` 的逻辑处理。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.029</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.controller</span><span class="variable">.UserController</span>      : [exception03]</span><br><span class="line"></span><br><span class="line"><span class="comment">//【不存在】之后，按照 HandlerInterceptor 链的**倒序**，执行 `#postHandle(...)` 方法。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 最后，按照 HandlerInterceptor 链的**倒序**，执行 `#afterCompletion(...)` 方法。</span></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.036</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.ThirdInterceptor</span>           : [afterCompletion][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.exception03</span>())]</span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.037</span> ERROR <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] o<span class="variable">.s</span><span class="variable">.web</span><span class="variable">.servlet</span><span class="variable">.HandlerExecutionChain</span>    : HandlerInterceptor<span class="variable">.afterCompletion</span> threw exception</span><br><span class="line">java<span class="variable">.lang</span><span class="variable">.RuntimeException</span>: 故意抛个错误 <span class="comment">// ... 省略异常堆栈</span></span><br><span class="line"></span><br><span class="line"><span class="number">2019</span>-<span class="number">11</span>-<span class="number">17</span> <span class="number">12</span>:<span class="number">54</span>:<span class="number">45</span><span class="variable">.037</span>  INFO <span class="number">28157</span> --- [nio-<span class="number">8080</span>-exec-<span class="number">7</span>] c<span class="variable">.i</span><span class="variable">.s</span><span class="variable">.l</span><span class="variable">.s</span><span class="variable">.c</span><span class="variable">.i</span><span class="variable">.FirstInterceptor</span>           : [afterCompletion][handler(public <span class="keyword">void</span> cn<span class="variable">.iocoder</span><span class="variable">.springboot</span><span class="variable">.lab23</span><span class="variable">.springmvc</span><span class="variable">.controller</span><span class="variable">.UserController</span><span class="variable">.exception03</span>())]</span><br></pre></td></tr></table></figure><ul><li><code>handler</code> 的逻辑处理，抛出 Exception 异常的情况，HandlerInterceptor 的 <code>#postHandle(...)</code> 方法不会执行。为什么呢？答案我们已经在 <code>/users/current_user</code> 接口中，进行了说明。</li></ul><h2 id="6-4-拓展阅读"><a href="#6-4-拓展阅读" class="headerlink" title="6.4 拓展阅读"></a>6.4 拓展阅读</h2><p>这里，艿艿整理了一些不同场景下的 HandlerInterceptor 实现类，胖友可以选择性看看：</p><ul><li>限流拦截器：<a href="http://www.iocoder.cn/Fight/Spring-Boot-project-API-anti-brush-interface/?self" target="_blank" rel="noopener">《Spring Boot 项目的 API 接口防刷》</a></li><li>访问日志拦截器：<a href="https://github.com/YunaiV/onemall/blob/master/common/mall-spring-boot/src/main/java/cn/iocoder/mall/spring/boot/web/interceptor/AccessLogInterceptor.java" target="_blank" rel="noopener"><code>AccessLogInterceptor.java</code></a></li><li>用户认证 + 授权拦截器：<a href="https://github.com/YunaiV/onemall/blob/master/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java" target="_blank" rel="noopener"><code>UserSecurityInterceptor.java</code></a></li><li>管理员认证 + 授权拦截器：<a href="https://github.com/YunaiV/onemall/blob/master/system/system-sdk/src/main/java/cn/iocoder/mall/admin/sdk/interceptor/AdminSecurityInterceptor.java" target="_blank" rel="noopener"><code>AdminSecurityInterceptor.java</code></a></li></ul><p>如果胖友有其他场景下的 HandlerInterceptor 示例，欢迎留言。</p><h1 id="7-Servlet、Filter、Listener"><a href="#7-Servlet、Filter、Listener" class="headerlink" title="7. Servlet、Filter、Listener"></a><span id="anchor-7">7. Servlet、Filter、Listener</span></h1><p>虽然绝大多数情况下，我们无需在 SpringMVC 中，直接使用 <a href="https://docs.oracle.com/javaee/7/api/javax/servlet/package-summary.html" target="_blank" rel="noopener"><code>java.servlet</code></a> 提供的 Servlet、Filter、Listener 了，但是在使用一些三方类库时，它们更多的提供的是 <a href="https://docs.oracle.com/javaee/7/api/javax/servlet/package-summary.html" target="_blank" rel="noopener"><code>java.servlet</code></a> 中的组件，毕竟它们需要提供更加公用的组件，而不能和 SpringMVC 耦合在一起。</p><p>例如说，在使用 <a href="https://shiro.apache.org/" target="_blank" rel="noopener">Shiro</a> 做权限认证相关方面的功能时，我们就需要配置 Shiro 提供的 <a href="https://github.com/apache/shiro/blob/master/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java" target="_blank" rel="noopener">ShiroFilterFactoryBean</a> 。</p><p>我们有<strong>两种</strong>方式，使用 Java 代码的方式，配置 Servlet、Filter、Listener 。分别是：</p><ul><li>通过 Bean 的方式</li><li>通过注解的方式</li></ul><blockquote><p>友情提示：该示例，基于 <a href="#anchor-6">「6. HandlerInterceptor 拦截器」</a> 的 <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 的基础上，继续改造。</p></blockquote><h2 id="7-1-通过-Bean-的方式"><a href="#7-1-通过-Bean-的方式" class="headerlink" title="7.1 通过 Bean 的方式"></a>7.1 通过 Bean 的方式</h2><p>在 <code>SpringMVCConfiguration</code> 配置类中，我们来添加 Servlet、Filter、Listener 三个 Bean 的配置。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> ServletRegistrationBean <span class="title">testServlet01</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    ServletRegistrationBean servletRegistrationBean = <span class="keyword">new</span> ServletRegistrationBean&lt;&gt;(<span class="keyword">new</span> HttpServlet() &#123;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException </span>&#123;</span><br><span class="line">            logger.info(<span class="string">"[doGet][uri: &#123;&#125;]"</span>, req.getRequestURI());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;);</span><br><span class="line">    servletRegistrationBean.setUrlMappings(Collections.singleton(<span class="string">"/test/01"</span>));</span><br><span class="line">    <span class="keyword">return</span> servletRegistrationBean;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> FilterRegistrationBean <span class="title">testFilter01</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    FilterRegistrationBean filterRegistrationBean = <span class="keyword">new</span> FilterRegistrationBean&lt;&gt;(<span class="keyword">new</span> Filter() &#123;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException </span>&#123;</span><br><span class="line">            logger.info(<span class="string">"[doFilter]"</span>);</span><br><span class="line">            filterChain.doFilter(servletRequest, servletResponse);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;);</span><br><span class="line">    filterRegistrationBean.setUrlPatterns(Collections.singleton(<span class="string">"/test/*"</span>));</span><br><span class="line">    <span class="keyword">return</span> filterRegistrationBean;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> ServletListenerRegistrationBean&lt;?&gt; testListener01() &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> ServletListenerRegistrationBean&lt;&gt;(<span class="keyword">new</span> ServletContextListener() &#123;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">contextInitialized</span><span class="params">(ServletContextEvent sce)</span> </span>&#123;</span><br><span class="line">            logger.info(<span class="string">"[contextInitialized]"</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">contextDestroyed</span><span class="params">(ServletContextEvent sce)</span> </span>&#123;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><p>在 Spring Boot 中，提供了 <a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletRegistrationBean.java" target="_blank" rel="noopener">ServletRegistrationBean</a> 来配置 Servlet Bean、<a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/FilterRegistrationBean.java" target="_blank" rel="noopener">FilterRegistrationBean</a> 来配置 Filter Bean、<a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletListenerRegistrationBean.java" target="_blank" rel="noopener">ServletListenerRegistrationBean</a> 来配置 Listener Bean 。</p></li><li><p>这里，我们为了让示例比较简洁，所以采用了内部类。实际在使用时，还是正常去定义类。</p></li></ul><h2 id="7-2-通过注解的方式"><a href="#7-2-通过注解的方式" class="headerlink" title="7.2 通过注解的方式"></a>7.2 通过注解的方式</h2><p>在 Servlet3.0 的新特性里，提供了 <a href="https://github.com/javaee/servlet-spec/blob/master/src/main/java/javax/servlet/annotation/WebServlet.java" target="_blank" rel="noopener">@WebServlet</a>、<a href="https://github.com/javaee/servlet-spec/blob/master/src/main/java/javax/servlet/annotation/WebFilter.java" target="_blank" rel="noopener">@WebFilter</a>、<a href="https://github.com/javaee/servlet-spec/blob/master/src/main/java/javax/servlet/annotation/WebListener.java" target="_blank" rel="noopener">@WebListener</a> 三个注解，方便配置 Servlet、Filter、Listener 。</p><p>而在 SpringBoot 中，我们仅需要在 Application 类上，添加 <a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletComponentScan.java" target="_blank" rel="noopener">@ServletComponentScan</a> 注解，开启对 <code>@WebServlet</code>、<code>@WebFilter</code>、<code>@WebListener</code> 注解的扫描。不过要<strong>注意</strong>，当且仅当使用<strong>内嵌</strong>的 Web Server 才会生效。</p><p>我们创建了三个示例。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// TestServlet02.java</span></span><br><span class="line"><span class="meta">@WebServlet</span>(urlPatterns = <span class="string">"/test/02"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestServlet02</span> <span class="keyword">extends</span> <span class="title">HttpServlet</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[doGet][uri: &#123;&#125;]"</span>, req.getRequestURI());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TestFilter02.java</span></span><br><span class="line"><span class="meta">@WebFilter</span>(<span class="string">"/test/*"</span>)</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestFilter02</span> <span class="keyword">implements</span> <span class="title">Filter</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[doFilter]"</span>);</span><br><span class="line">        filterChain.doFilter(servletRequest, servletResponse);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TestServletContextListener02.java</span></span><br><span class="line"><span class="meta">@WebListener</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestServletContextListener02</span> <span class="keyword">implements</span> <span class="title">ServletContextListener</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Logger logger = LoggerFactory.getLogger(getClass());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">contextInitialized</span><span class="params">(ServletContextEvent sce)</span> </span>&#123;</span><br><span class="line">        logger.info(<span class="string">"[contextInitialized]"</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">contextDestroyed</span><span class="params">(ServletContextEvent sce)</span> </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="8-Cors-跨域"><a href="#8-Cors-跨域" class="headerlink" title="8. Cors 跨域"></a>8. Cors 跨域</h1><blockquote><p>示例代码对应仓库： <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p></blockquote><p>在前后端分离之后，我们会碰到跨域的问题。例如说，前端在 <a href="http://www.iocoder.cn/" target="_blank" rel="noopener">http://www.iocoder.cn</a> 域名下，而后端 API 在 <a href="http://api.iocoder.cn/" target="_blank" rel="noopener">http://api.iocoder.cn</a> 域名下。</p><p>对跨域不是很了解的胖友，可以看看阮大的 <a href="https://www.ruanyifeng.com/blog/2016/04/cors.html" target="_blank" rel="noopener">《跨域资源共享 CORS 详解》</a> 文章。 当然，也可以先继续本文的阅读。</p><p>解决跨域的方式有很多，例如说，在 Nginx 上配置处理跨域请求的参数。又例如说，项目中有网关服务，统一配置处理。当然，本文既然是 Spring Boot SpringMVC 入门，那么必然只使用 SpringMVC 来解决跨域。目前一共有三种方案：</p><ul><li>方式一，使用 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/bind/annotation/CrossOrigin.java" target="_blank" rel="noopener">@CrossCors</a> 注解，配置每个 API 接口。</li><li>方式二，使用 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java" target="_blank" rel="noopener">CorsRegistry</a> 注册表，配置每个 API 接口。</li><li>方案三，使用 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java" target="_blank" rel="noopener">CorsFilter</a> <strong>过滤器</strong>，处理跨域请求。</li></ul><blockquote><p>友情提示：该示例，基于 <a href="#anchor-7">「7. Servlet、Filter、Listener」</a> 的 <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 的基础上，继续改造。</p></blockquote><h2 id="8-1-CrossCors"><a href="#8-1-CrossCors" class="headerlink" title="8.1 @CrossCors"></a>8.1 @CrossCors</h2><p><code>@CrossCors</code> 注解，添加在类或方法上，标记该类/方法对应接口的 Cors 信息。</p><p><code>@CrossCors</code> 注解的<strong>常用属性</strong>，如下：</p><ul><li><code>origins</code> 属性，设置允许的请求来源。<code>[]</code> 数组，可以填写多个请求来源。默认值为 <code>*</code> 。</li><li><code>value</code> 属性，和 <code>origins</code> 属性相同，是它的别名。</li><li><code>allowCredentials</code> 属性，是否允许客户端请求发送 Cookie 。默认为 <code>false</code> ，不允许请求发送 Cookie 。</li><li><code>maxAge</code> 属性，本次预检请求的有效期，单位为秒。默认值为 1800 秒。</li></ul><p><code>@CrossCors</code> 注解的<strong>不常用属性</strong>，如下：</p><ul><li><code>methods</code> 属性，设置允许的请求方法。<code>[]</code> 数组，可以填写多个请求方法。默认值为 <code>GET</code> + <code>POST</code> 。</li><li><code>allowedHeaders</code> 属性，允许的请求头 Header 。<code>[]</code> 数组，可以填写多个请求来源。默认值为 <code>*</code> 。</li><li><code>exposedHeaders</code> 属性，允许的响应头 Header 。<code>[]</code> 数组，可以填写多个请求来源。默认值为 <code>*</code> 。</li></ul><p>一般情况下，我们在<strong>每个</strong> Controller 上，添加 <code>@CrossCors</code> 注解即可。当然，如果某个 API 接口希望做自定义的配置，可以在 Method 方法上添加。示例如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// TestController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping</span>(<span class="string">"/test"</span>)</span><br><span class="line"><span class="meta">@CrossOrigin</span>(origins = <span class="string">"*"</span>, allowCredentials = <span class="string">"true"</span>) <span class="comment">// 允许所有来源，允许发送 Cookie</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestController</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获得指定用户编号的用户</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping</span>(<span class="string">"/get"</span>)</span><br><span class="line">    <span class="meta">@CrossOrigin</span>(allowCredentials = <span class="string">"false"</span>) <span class="comment">// 允许所有来源，不允许发送 Cookie</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> UserVO <span class="title">get</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> UserVO().setId(<span class="number">1</span>).setUsername(UUID.randomUUID().toString());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>在绝大数场合下，肯定是在 Controller 上，添加 <code>@CrossOrigin(allowCredentials = &quot;true&quot;)</code> 即可</strong>。</p><p><strong>坑坑坑坑坑</strong></p><p>在前端使用符合 CORS 规范的网络库时，例如说 Vue 常用的网络库 <a href="https://github.com/axios/axios" target="_blank" rel="noopener">axios</a> ，在发起<a href="https://www.ruanyifeng.com/blog/2016/04/cors.html" target="_blank" rel="noopener">非简单请求</a>时，会自动先先发起 <code>OPTIONS</code> “预检”请求，要求服务器确认是否能够这样请求。这样，这个请求就会被 SpringMVC 的拦截器所处理。</p><p>此时，如果我们的拦截器认为 <code>handler</code> <strong>一定</strong>是 HandlerMethod 类型时，就会导致报错。例如说，「芋道源码」在 <a href="https://github.com/YunaiV/onemall/blob/master/user/user-sdk/src/main/java/cn/iocoder/mall/user/sdk/interceptor/UserSecurityInterceptor.java" target="_blank" rel="noopener">UserSecurityInterceptor</a> 拦截器中，会认为 <code>handler</code> 是 HandlerMethod 类型，然后通过它获得 <code>@RequiresLogin</code> 注解信息，判断是否需要登陆。然后，实际上，此时 <code>handler</code> 是 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java#L539-L558" target="_blank" rel="noopener">PreFlightHandler</a> 类型(SpringMVC会将Option请求的 <code>handler</code> 转成 <code>PreFlightHandler</code>)，则会导致抛出异常。如下图所示：<img src="3.png" alt="getCorsHandlerExecutionChain"></p><p>此时，有两种解决方案：</p><ol><li>检查每个拦截器的实现，是不是依赖于 <code>handler</code> 是 HandlerMethod 的逻辑，进行修复。</li><li>不使用该方案，而是采用 <a href="#anchor-8.3">「8.3 CorsFilter」</a> 过滤器，避免 <code>OPTIONS</code> 预检查走到拦截器里。</li></ol><p>显然，<code>1</code> 的成本略微有点高，所以一般情况下，推荐 <code>2</code> 。</p><p>在 <a href="https://segmentfault.com/a/1190000010348077" target="_blank" rel="noopener">《【SpringMVC】与权限拦截器冲突导致的 Cors 跨域设置失效问题》</a> 文章中，我们也可以看到有开发者，碰到一样的问题。</p><h2 id="8-2-CorsRegistry"><a href="#8-2-CorsRegistry" class="headerlink" title="8.2 CorsRegistry"></a>8.2 CorsRegistry</h2><p>显然，在每个 Controller 上配置 <code>@CrossOrigin</code> 注解，是挺麻烦一事。所以更多的情况下，我们会选择配置 CorsRegistry 注册表。</p><p>修改 <code>SpringMVCConfiguration</code> 配置类，增加 CorsRegistry 相关的配置。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addCorsMappings</span><span class="params">(CorsRegistry registry)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 添加全局的 CORS 配置</span></span><br><span class="line">    registry.addMapping(<span class="string">"/**"</span>) <span class="comment">// 匹配所有 URL ，相当于全局配置</span></span><br><span class="line">            .allowedOrigins(<span class="string">"*"</span>) <span class="comment">// 允许所有请求来源</span></span><br><span class="line">            .allowCredentials(<span class="keyword">true</span>) <span class="comment">// 允许发送 Cookie</span></span><br><span class="line">            .allowedMethods(<span class="string">"*"</span>) <span class="comment">// 允许所有请求 Method</span></span><br><span class="line">            .allowedHeaders(<span class="string">"*"</span>) <span class="comment">// 允许所有请求 Header</span></span><br><span class="line"><span class="comment">//                .exposedHeaders("*") // 允许所有响应 Header</span></span><br><span class="line">            .maxAge(<span class="number">1800L</span>); <span class="comment">// 有效期 1800 秒，2 小时</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>这里配置匹配的路径是 <code>/**</code> ，从而实现全局 CORS 配置。</li><li>如果想要配置单个路径的 CORS 配置，可以通过 <code>CorsRegistry#addMapping(String pathPattern)</code> 方法，继续往其中添加 CORS 配置。</li><li>如果胖友想要更安全，可以 <code>originns</code> 属性，只填写允许的前端域名地址。</li></ul><p>这种方式，一样会存在 <a href="#anchor-8.1">「8.1 @CrossCors」</a> 提供到的坑坑坑坑坑，因为这两者的实现方式是一致的。所以，继续看 <a href="#anchor-8.3">「8.3 CorsFilter」</a> 方案。</p><h2 id="8-3-CorsFilter"><a href="#8-3-CorsFilter" class="headerlink" title="8.3 CorsFilter"></a>8.3 CorsFilter</h2><p>在 <a href="https://github.com/spring-projects/spring-framework/tree/master/spring-web" target="_blank" rel="noopener">Spring Web</a> 中，内置提供 CorsFilter 过滤器，实现对 CORS 的处理。</p><p>配置方式很简单，既然是 Filter 过滤器，就可以采用 <a href="#anchor-7.1">「7.1 通过 Bean 的方式」</a> ，进行配置。所以修改 <code>SpringMVCConfiguration</code> 配置类，增加 CorsFilter 相关的配置。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> FilterRegistrationBean&lt;CorsFilter&gt; <span class="title">corsFilter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 创建 UrlBasedCorsConfigurationSource 配置源，类似 CorsRegistry 注册表</span></span><br><span class="line">    UrlBasedCorsConfigurationSource source = <span class="keyword">new</span> UrlBasedCorsConfigurationSource();</span><br><span class="line">    <span class="comment">// 创建 CorsConfiguration 配置，相当于 CorsRegistration 注册信息</span></span><br><span class="line">    CorsConfiguration config = <span class="keyword">new</span> CorsConfiguration();</span><br><span class="line">    config.setAllowedOrigins(Collections.singletonList(<span class="string">"*"</span>)); <span class="comment">// 允许所有请求来源</span></span><br><span class="line">    config.setAllowCredentials(<span class="keyword">true</span>); <span class="comment">// 允许发送 Cookie</span></span><br><span class="line">    config.addAllowedMethod(<span class="string">"*"</span>); <span class="comment">// 允许所有请求 Method</span></span><br><span class="line">    config.setAllowedHeaders(Collections.singletonList(<span class="string">"*"</span>)); <span class="comment">// 允许所有请求 Header</span></span><br><span class="line">    <span class="comment">// config.setExposedHeaders(Collections.singletonList("*")); // 允许所有响应 Header</span></span><br><span class="line">    config.setMaxAge(<span class="number">1800L</span>); <span class="comment">// 有效期 1800 秒，2 小时</span></span><br><span class="line">    source.registerCorsConfiguration(<span class="string">"/**"</span>, config);</span><br><span class="line">    <span class="comment">// 创建 FilterRegistrationBean 对象</span></span><br><span class="line">    FilterRegistrationBean&lt;CorsFilter&gt; bean = <span class="keyword">new</span> FilterRegistrationBean&lt;&gt;(</span><br><span class="line">            <span class="keyword">new</span> CorsFilter(source)); <span class="comment">// 创建 CorsFilter 过滤器</span></span><br><span class="line">    bean.setOrder(<span class="number">0</span>); <span class="comment">// 设置 order 排序。这个顺序很重要哦，为避免麻烦请设置在最前</span></span><br><span class="line">    <span class="keyword">return</span> bean;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>已经添加了详细的注释，胖友自己看下噢。效果上，和 <a href="#anchor-8.2">「8.2 CorsRegistry」</a> 是一致的。</li></ul><p>至此，我们已经学习完三种 SpringMVC 配置 CORS 的方式。<strong>结论就是，使用 <a href="#anchor-8.3">「8.3 CorsFilter」</a> 方式。</strong> 。最后在推荐一篇文章，感觉写的不错 <a href="https://segmentfault.com/a/1190000019485883" target="_blank" rel="noopener">《Spring 里那么多种 CORS 的配置方式，到底有什么区》</a> 。</p><h1 id="9-HttpMessageConverter-消息转换器"><a href="#9-HttpMessageConverter-消息转换器" class="headerlink" title="9. HttpMessageConverter 消息转换器"></a>9. HttpMessageConverter 消息转换器</h1><blockquote><p>示例代码对应仓库：<a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p></blockquote><p>在 Spring MVC 中，可以使用 <code>@RequestBody</code> 和 <code>@ResponseBody</code> 两个注解，分别完成<strong>请求报（内容）到对象</strong>和<strong>对象到响应报文（内容）</strong>的转换，底层这种灵活的消息转换机制，就是 Spring 3.x 中新引入的 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java" target="_blank" rel="noopener">HttpMessageConverter</a> ，即消息转换器机制。</p><p>这么解释，胖友肯定是不会买单的。😜 在上面的示例里，我们已经看到，我们明明返回的是 UserVO 对象，最后输出给前端时，变成了 JSON 字符串，这就是使用了 <a href="https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java" target="_blank" rel="noopener">MappingJackson2HttpMessageConverter</a> 消息转换器，将 UserVO 对象转换成 JSON 字符串，写回给前端。 现在是不是有点理解了？！</p><p>在一些业务场景下，前端提交给后端 API 参数，比较复杂，那么可能我们希望能够使用 JSON 的格式，提交给后端 API 接口。此时，我们又可以使用 MappingJackson2HttpMessageConverter 消息转换器，将 JSON 字符串，转换成对应的对象。现在是不是更加清晰了!？</p><p>让我们再来一起看看 HttpMessageConverter 消息转换器接口，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// HttpMessageConverter.java</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 是否能够读取指定的 mediaType 内容类型，转换成对应的 clazz 对象</span></span><br><span class="line"><span class="function"><span class="keyword">boolean</span> <span class="title">canRead</span><span class="params">(Class&lt;?&gt; clazz, @Nullable MediaType mediaType)</span></span>;</span><br><span class="line"><span class="comment">// 读取请求内容，转换成 clazz 对象</span></span><br><span class="line"><span class="function">T <span class="title">read</span><span class="params">(Class&lt;? extends T&gt; clazz, HttpInputMessage inputMessage)</span></span></span><br><span class="line"><span class="function">			<span class="keyword">throws</span> IOException, HttpMessageNotReadableException</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 是否能够将 clazz 对象，序列化成 mediaType 内容类型</span></span><br><span class="line"><span class="function"><span class="keyword">boolean</span> <span class="title">canWrite</span><span class="params">(Class&lt;?&gt; clazz, @Nullable MediaType mediaType)</span></span>;</span><br><span class="line"><span class="comment">// 将 clazz 对象，序列化成 contentType 内容类型，写入到响应。</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">write</span><span class="params">(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)</span></span></span><br><span class="line"><span class="function">			<span class="keyword">throws</span> IOException, HttpMessageNotWritableException</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获得 HttpMessageConverter 能够支持的内容类型。</span></span><br><span class="line"><span class="function">List&lt;MediaType&gt; <span class="title">getSupportedMediaTypes</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><ul><li>在<strong>请求</strong>时，我们在请求头 <code>Content-Type</code> 上，表示请求内容（Request Body）的内容类型。这样，SpringMVC 会从自己的 HttpMessageConverter <strong>数组</strong>中，通过 <code>#canRead(clazz, mediaType)</code> 方法，判断是否够读取指定的 <code>mediaType</code> 内容类型，转换成对应的 <code>clazz</code> 对象。如果可以，则调用 <code>#read(Class clazz, HttpInputMessage inputMessage)</code> 方法，读取请求内容，转换成 <code>clazz</code> 对象。</li><li>在<strong>响应</strong>时，我们在请求头 <code>Accept</code> 上，表示请求内容（Response Body）的内容类型。这样，SpringMVC 会从自己的 HttpMessageConverter <strong>数组</strong>中，通过 <code>#canWrite(clazz, mediaType)</code> 方法，判断是否能够将 <code>clazz</code> 对象，序列化成 <code>mediaType</code> 内容类型。如果可以，则调用 <code>#write(contentType, outputMessage)</code> 方法， 将 <code>clazz</code> 对象，序列化成 <code>contentType</code> 内容类型，写入到响应。</li></ul><blockquote><p>友情提示：该示例，基于 <a href="http://www.iocoder.cn/Spring-Boot/SpringMVC/?github#" target="_blank" rel="noopener">「8. Cors 跨域」</a> 的 <a href="https://github.com/YunaiV/SpringBoot-Labs/tree/master/lab-23/lab-springmvc-23-02" target="_blank" rel="noopener">lab-springmvc-23-02</a> 的基础上，继续改造。</p></blockquote><h2 id="9-1-引入依赖"><a href="#9-1-引入依赖" class="headerlink" title="9.1 引入依赖"></a>9.1 引入依赖</h2><p>在 <code>pom.xml</code> 文件中，<strong>额外</strong>引入 <code>jackson-dataformat-xml</code> 依赖。如下：</p><blockquote><p>当我们使用浏览器访问接口时，会发现返回的是xml格式，使用<code>F12</code>调试你会发现，请求头类型是<code>xml</code>，我们通过通过在接口上指定 <code>produces</code> 属性，如</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping</span>(value = <span class="string">"json"</span>,</span><br><span class="line">    produces = &#123;MediaType.APPLICATION_JSON_VALUE&#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure></blockquote><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 引入 jackson 对 xml 的转换器，实现对 XML 的序列化 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.fasterxml.jackson.dataformat<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jackson-dataformat-xml<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="9-2-SpringMVCConfiguration"><a href="#9-2-SpringMVCConfiguration" class="headerlink" title="9.2 SpringMVCConfiguration"></a>9.2 SpringMVCConfiguration</h2><p>修改 <code>SpringMVCConfiguration</code> 配置类，增加 MappingJackson2XmlHttpMessageConverter 相关的配置，用于 XML 格式的 HttpMessageConverter 消息转换器。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">configureMessageConverters</span><span class="params">(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 增加 XML 消息转换器</span></span><br><span class="line">    Jackson2ObjectMapperBuilder xmlBuilder = Jackson2ObjectMapperBuilder.xml();</span><br><span class="line">    xmlBuilder.indentOutput(<span class="keyword">true</span>);</span><br><span class="line">    converters.add(<span class="keyword">new</span> MappingJackson2XmlHttpMessageConverter(xmlBuilder.build()));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="9-3-UserController"><a href="#9-3-UserController" class="headerlink" title="9.3 UserController"></a>9.3 UserController</h2><p>在 <code>UserController</code> 类中，我们添加一个 API 接口，新增用户，方便我们 XML/JSON 格式的请求和响应。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// UserController.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@PostMapping</span>(value = <span class="string">"/add"</span>,</span><br><span class="line">        <span class="comment">// ↓ 增加 "application/xml"、"application/json" ，针对 Content-Type 请求头</span></span><br><span class="line">        consumes = &#123;MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE&#125;,</span><br><span class="line">        <span class="comment">// ↓ 增加 "application/xml"、"application/json" ，针对 Accept 请求头</span></span><br><span class="line">        produces = &#123;MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE&#125;</span><br><span class="line">)</span><br><span class="line"><span class="function"><span class="keyword">public</span> UserVO <span class="title">add</span><span class="params">(@RequestBody UserVO user)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> user;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>请求<ul><li>在接口参数上，我们添加了 <code>@RequestBody</code> 注解。</li><li>添加 <code>consumes</code> 属性，增加 <code>&quot;application/xml&quot;</code>、<code>&quot;application/json&quot;</code> ，针对 <code>Content-Type</code> 请求头。</li></ul></li><li>响应<ul><li>因为 UserController 已经添加了 <code>@RestController</code> 注解，我们无需重复给改 API 接口，添加 <code>@ReponseBody</code> 注解。</li><li>添加 <code>produces</code> 属性，增加 <code>&quot;application/xml&quot;</code>、<code>&quot;application/json&quot;</code> ，针对 <code>Accept</code> 请求头。</li></ul></li></ul><p>下面，我们使用 <a href="https://www.getpostman.com/" target="_blank" rel="noopener">Postman</a> ，模拟请求。</p><p><strong>① JSON 格式请求，JSON 格式响应</strong></p><p><img src="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/04.png" alt="JSON + JSON"></p><p><strong>② XML 格式请求，XML 格式响应</strong></p><p><img src="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/05.png" alt="XML + XML"></p><p><strong>③ JSON 格式请求，XML 格式响应</strong></p><p><img src="http://www.iocoder.cn/images/Spring-Boot/2019-11-17/06.png" alt="JSON + XML"></p><h1 id="10-整合-Fastjson"><a href="#10-整合-Fastjson" class="headerlink" title="10. 整合 Fastjson"></a>10. 整合 Fastjson</h1><blockquote><p>示例代码对应仓库：<a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 。</p><p>参考文章 <a href="https://github.com/alibaba/fastjson/wiki/在-Spring-中集成-Fastjson" target="_blank" rel="noopener">《在 Spring 中集成 Fastjson》</a></p><p>友情提示：该示例，基于 <a href="#anchor-8">「8. Cors 跨域」</a> 的 <a href="https://github.com/yuyong725/labs-springboot2/tree/master/labs-springboot2-web/labs-springboot2-springmvc/labs-springboot2-springmvc-advanced" target="_blank" rel="noopener">labs-springboot2-springmvc-advanced</a> 的基础上，继续改造。</p></blockquote><h2 id="10-1-引入依赖"><a href="#10-1-引入依赖" class="headerlink" title="10.1 引入依赖"></a>10.1 引入依赖</h2><p>在 <code>pom.xml</code>文件中，<strong>额外</strong>引入 <code>fastjson</code> 依赖。如下：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 引入 Fastjson ，实现对 JSON 的序列化 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>fastjson<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.62<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="10-2-SpringMVCConfiguration"><a href="#10-2-SpringMVCConfiguration" class="headerlink" title="10.2 SpringMVCConfiguration"></a>10.2 SpringMVCConfiguration</h2><p>修改 <code>SpringMVCConfiguration</code> 配置类，增加 FastJsonHttpMessageConverter 相关的配置。代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// SpringMVCConfiguration.java</span></span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">configureMessageConverters</span><span class="params">(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters)</span> </span>&#123;</span><br><span class="line">    <span class="comment">/// 希望 xml 和 json 同时生效，就在一个方法里面设置</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建 FastJsonHttpMessageConverter 对象</span></span><br><span class="line">    FastJsonHttpMessageConverter fastJsonHttpMessageConverter = <span class="keyword">new</span> FastJsonHttpMessageConverter();</span><br><span class="line">    <span class="comment">// 自定义 FastJson 配置</span></span><br><span class="line">    FastJsonConfig fastJsonConfig = <span class="keyword">new</span> FastJsonConfig();</span><br><span class="line">    <span class="comment">// 设置字符集</span></span><br><span class="line">    fastJsonConfig.setCharset(Charset.defaultCharset());</span><br><span class="line">    <span class="comment">// 剔除循环引用</span></span><br><span class="line">    fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);</span><br><span class="line">    fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);</span><br><span class="line">    <span class="comment">// 设置支持的 MediaType</span></span><br><span class="line">    fastJsonHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON,</span><br><span class="line">            MediaType.APPLICATION_JSON_UTF8));</span><br><span class="line">    <span class="comment">// 添加到 converters 中，注意，添加到最开头，放在 MappingJackson2XmlHttpMessageConverter 前面</span></span><br><span class="line">    converters.add(<span class="number">0</span>, fastJsonHttpMessageConverter);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此，SpringMVC 整合 Fastjson 已经完成，还是蛮方便的。</p><h1 id="666-彩蛋"><a href="#666-彩蛋" class="headerlink" title="666. 彩蛋"></a>666. 彩蛋</h1><p>在我们设计后端 API 接口的时候，我们可以参考一些大厂的开放平台，设计出更合理更优雅的 API 接口。例如说：</p><ul><li><a href="https://doc.youzanyun.com/doc#/content/API/1-331" target="_blank" rel="noopener">《有赞云 —— API 列表》</a></li><li><a href="https://docs.open.alipay.com/api_1" target="_blank" rel="noopener">《支付宝 —— 开放平台 API 列表》</a></li><li><a href="http://open.jd.com/home/home#/doc/api" target="_blank" rel="noopener">《京东 —— 宙斯 API 列表》</a></li><li><a href="https://open.dianping.com/document/v2?docId=6000346&rootDocId=5000" target="_blank" rel="noopener">《美团点评 —— API 文档》</a></li><li><a href="https://open.weibo.com/wiki/API" target="_blank" rel="noopener">《新浪微博 —— API 文档》</a></li></ul><p>如果本文有写的不正确，或者让胖友误解的地方，欢迎给卡姆留言。</p></div><div class="article-footer"><blockquote class="mt-2x"><ul class="post-copyright list-unstyled"><li class="post-copyright-link hidden-xs"><strong>本文链接：</strong> <a href="https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/" title="Spring Boot SpringMVC 入门" target="_blank" rel="external">https://calm_java.gitee.io/blog/2019/12/29/labs-springboot2-SpringMVC/</a></li><li class="post-copyright-license"><strong>版权声明： </strong>本博客所有文章除特别声明外，均采用 <a href="http://creativecommons.org/licenses/by/4.0/deed.zh" target="_blank" rel="external">CC BY 4.0 CN协议</a> 许可协议。转载请注明出处！</li></ul></blockquote><div class="panel panel-default panel-badger"><div class="panel-body"><figure class="media"><div class="media-left"><a href="https://github.com/yuyong725" target="_blank" class="img-burn thumb-sm visible-lg"><img src="/blog/images/avatar.jpg" class="img-rounded w-full" alt=""></a></div><div class="media-body"><h3 class="media-heading"><a href="https://github.com/yuyong725" target="_blank"><span class="text-dark">卡姆</span><small class="ml-1x">a Java coder need calm</small></a></h3><div>享受编程</div></div></figure></div></div></div></article><section id="comments"></section></div><nav class="bar bar-footer clearfix" data-stick-bottom><div class="bar-inner"><ul class="pager pull-left"><li class="prev"><a href="/blog/2019/12/29/labs-springboot2-security/" title="Spring Boot 安全框架 Spring Security 入门"><i class="icon icon-angle-left" aria-hidden="true"></i><span>&nbsp;&nbsp;上一篇</span></a></li><li class="next"><a href="/blog/2019/12/27/jenkins-docker-boot/" title="jerkins+docker的持续化部署"><span>下一篇&nbsp;&nbsp;</span><i class="icon icon-angle-right" aria-hidden="true"></i></a></li><li class="toggle-toc"><a class="toggle-btn" data-toggle="collapse" href="#collapseToc" aria-expanded="true" title="文章目录" role="button"><span>[&nbsp;</span><span>文章目录</span> <i class="text-collapsed icon icon-anchor"></i> <i class="text-in icon icon-close"></i> <span>]</span></a></li></ul><button type="button" class="btn btn-fancy btn-donate pop-onhover bg-gradient-warning" data-toggle="modal" data-target="#donateModal"><span>赏</span></button><div class="bar-right"><div class="share-component" data-sites="wechat" data-mobile-sites="wechat"></div></div></div></nav><div class="modal modal-center modal-small modal-xs-full fade" id="donateModal" tabindex="-1" role="dialog"><div class="modal-dialog" role="document"><div class="modal-content donate"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button><div class="modal-body"><div class="donate-box"><div class="donate-head"><p>感谢您的支持，我会继续努力的!</p></div><div class="tab-content"><div role="tabpanel" class="tab-pane fade active in" id="alipay"><div class="donate-payimg"><img src="/blog/images/donate/alipayimg.jpeg" alt="扫码支持" title="扫一扫"></div><p class="text-muted mv">扫码打赏，你说多少就多少</p><p class="text-grey">打开支付宝扫一扫，即可进行扫码打赏哦</p></div><div role="tabpanel" class="tab-pane fade" id="wechatpay"><div class="donate-payimg"><img src="/blog/images/donate/wechatpayimg.jpeg" alt="扫码支持" title="扫一扫"></div><p class="text-muted mv">扫码打赏，你说多少就多少</p><p class="text-grey">打开微信扫一扫，即可进行扫码打赏哦</p></div></div><div class="donate-footer"><ul class="nav nav-tabs nav-justified" role="tablist"><li role="presentation" class="active"><a href="#alipay" id="alipay-tab" role="tab" data-toggle="tab" aria-controls="alipay" aria-expanded="true"><i class="icon icon-alipay"></i> 支付宝</a></li><li role="presentation"><a href="#wechatpay" role="tab" id="wechatpay-tab" data-toggle="tab" aria-controls="wechatpay" aria-expanded="false"><i class="icon icon-wepay"></i> 微信支付</a></li></ul></div></div></div></div></div></div></main><footer class="footer" itemscope itemtype="http://schema.org/WPFooter"></footer><script src="//cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script><script>window.jQuery||document.write('<script src="js/jquery.min.js"><\/script>')</script><script src="/blog/js/plugin.min.js"></script><script src="/blog/js/application.js"></script><script>!function(T){var N={TRANSLATION:{POSTS:"文章",PAGES:"页面",CATEGORIES:"分类",TAGS:"标签",UNTITLED:"(未命名)"},ROOT_URL:"/blog/",CONTENT_URL:"/blog/content.json"};T.INSIGHT_CONFIG=N}(window)</script><script src="/blog/js/insight.js"></script><script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script><script src="//cdn.jsdelivr.net/npm/gitalk@1.4.0/dist/gitalk.min.js"></script><script src="//cdn.jsdelivr.net/npm/blueimp-md5@2.10.0/js/md5.min.js"></script><script type="text/javascript">var gitalk=new Gitalk({clientID:"adf0c8bfdd8796029917",clientSecret:"a88c2e2e83e2b180f62ad927dcb347871dfe9f4b",repo:"myblog-comment",owner:"yuyong725",admin:["yuyong725"],id:md5(location.pathname),distractionFreeMode:!0});gitalk.render("comments")</script></body></html><!-- rebuild by neat -->